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

405
README.md
View File

@ -4,43 +4,92 @@ A modern, enterprise-grade approval and request management system built with Rea
## 📋 Table of Contents
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Development](#development)
- [Project Structure](#project-structure)
- [Available Scripts](#available-scripts)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [Features](#-features)
- [Tech Stack](#-tech-stack)
- [Prerequisites](#-prerequisites)
- [Installation](#-installation)
- [Development](#-development)
- [Project Structure](#-project-structure)
- [Available Scripts](#-available-scripts)
- [Configuration](#-configuration)
- [Key Features Deep Dive](#-key-features-deep-dive)
- [Troubleshooting](#-troubleshooting)
- [Contributing](#-contributing)
## ✨ Features
- **🔄 Dual Workflow System**
- Custom Request Workflow with user-defined approvers
- Claim Management Workflow (8-step predefined process)
### 🔄 Dual Workflow System
- **Custom Request Workflow** - User-defined approvers, spectators, and workflow steps
- **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**
- Real-time statistics and metrics
- High-priority alerts
- Recent activity tracking
### 📊 Comprehensive Dashboard
- Real-time statistics and metrics
- High-priority alerts and critical request tracking
- Recent activity feed with pagination
- Upcoming deadlines and SLA breach warnings
- Department-wise performance metrics
- Customizable KPI widgets (Admin only)
- **🎯 Request Management**
- Create, track, and manage approval requests
- Document upload and management
- Work notes and audit trails
- Spectator and stakeholder management
### 🎯 Request Management
- Create, track, and manage approval requests
- Document upload and management with file type validation
- Work notes and comprehensive audit trails
- Spectator and stakeholder management
- Request filtering, search, and export capabilities
- Detailed request lifecycle tracking
- **🎨 Modern UI/UX**
- Responsive design (mobile, tablet, desktop)
- Dark mode support
- Accessible components (WCAG compliant)
- Royal Enfield brand theming
### 👥 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
- **🔔 Notifications**
- Real-time toast notifications
- SLA tracking and reminders
- Approval status updates
### 📈 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)
- Dark mode support
- Accessible components (WCAG compliant)
- Royal Enfield brand theming
- Smooth animations and transitions
- Intuitive navigation and user flows
## 🛠️ Tech Stack
@ -50,8 +99,11 @@ A modern, enterprise-grade approval and request management system built with Rea
- **Styling:** Tailwind CSS 3.4+
- **UI Components:** shadcn/ui + Radix UI
- **Icons:** Lucide React
- **Notifications:** Sonner
- **State Management:** React Hooks (useState, useMemo)
- **Notifications:** Sonner (Toast) + Web Push API (VAPID)
- **Real-Time Communication:** Socket.IO Client
- **State Management:** React Hooks (useState, useMemo, useContext)
- **Authentication:** Okta SSO Integration
- **HTTP Client:** Axios
## 📦 Prerequisites
@ -74,7 +126,7 @@ A modern, enterprise-grade approval and request management system built with Rea
\`\`\`bash
git clone <repository-url>
cd Re_Figma_Code
cd Re_Frontend_Code
\`\`\`
### 2. Install dependencies
@ -147,11 +199,20 @@ VITE_PUBLIC_VAPID_KEY=your-production-vapid-key
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `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_CLIENT_ID` | Okta client ID for authentication | Yes* | - |
| `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
### 4. Verify setup
@ -213,28 +274,57 @@ Re_Figma_Code/
├── src/
│ ├── components/
│ │ ├── 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
│ │ ├── figma/ # Figma-specific components
│ │ ├── Dashboard.tsx
│ │ ├── Layout.tsx
│ │ ├── ClaimManagementWizard.tsx
│ │ ├── NewRequestWizard.tsx
│ │ ├── RequestDetail.tsx
│ │ ├── ClaimManagementDetail.tsx
│ │ ├── MyRequests.tsx
│ │ ├── participant/ # Participant management
│ │ ├── workflow/ # Workflow components
│ │ └── workNote/ # Work notes/chat components
│ ├── pages/
│ │ ├── Admin/ # Admin control panel
│ │ ├── ApproverPerformance/ # Approver analytics
│ │ ├── Auth/ # Authentication pages
│ │ ├── 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/
│ │ ├── customRequestDatabase.ts
│ │ ├── claimManagementDatabase.ts
│ │ └── dealerDatabase.ts
│ │ ├── socket.ts # Socket.IO utilities
│ │ ├── pushNotifications.ts # Web push notifications
│ │ ├── slaTracker.ts # SLA calculation utilities
│ │ └── ...
│ ├── contexts/
│ │ └── AuthContext.tsx # Authentication context
│ ├── styles/
│ │ └── globals.css
│ ├── types/
│ │ └── index.ts # TypeScript type definitions
│ │ └── index.ts
│ ├── App.tsx
│ └── main.tsx
├── public/ # Static assets
├── .vscode/ # VS Code settings
├── public/
│ └── service-worker.js # Service worker for push notifications
├── .vscode/
├── index.html
├── vite.config.ts
├── tsconfig.json
@ -267,7 +357,7 @@ The project uses path aliases for cleaner imports:
\`\`\`typescript
import { Button } from '@/components/ui/button';
import { getDealerInfo } from '@/utils/dealerDatabase';
import { getSocket } from '@/utils/socket';
\`\`\`
Path aliases are configured in:
@ -311,6 +401,7 @@ To connect to the backend API:
1. **Update API base URL** in `.env.local`:
\`\`\`env
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
@ -318,10 +409,24 @@ To connect to the backend API:
3. **Authentication:**
- Configure Okta credentials in environment variables
- 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/`
- All API calls use `axios` configured with base URL from environment
- WebSocket utilities in `src/utils/socket.ts`
### Development vs Production
@ -329,10 +434,181 @@ To connect to the backend API:
- **Production:** Uses `.env.production` or environment variables set in deployment platform
- **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
### 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
If the default port (5173) is in use:
@ -381,6 +657,35 @@ npm run type-check
- Verify all environment variables are set correctly
- Ensure Node.js and npm versions meet requirements
- 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)

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
setDynamicRequests([...dynamicRequests, newCustomRequest]);
console.log('New custom request created:', newCustomRequest);
navigate('/my-requests');
toast.success('Request Submitted Successfully!', {
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) => {
setTimeout(() => {
if (action === 'approve') {
@ -236,7 +235,6 @@ function AppRoutes({ onLogout }: AppProps) {
});
}
console.log(`${action} action completed with comment:`, comment);
setApprovalAction(null);
resolve(true);
}, 1000);
@ -658,8 +656,6 @@ interface MainAppProps {
export default function App(props?: MainAppProps) {
const { onLogout } = props || {};
console.log('🟢 Main App component rendered');
console.log('🟢 onLogout prop received:', !!onLogout);
return (
<BrowserRouter>

View File

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

View File

@ -32,7 +32,6 @@ export function AnalyticsConfig() {
const handleSave = () => {
// TODO: Implement API call to save configuration
console.log('Saving analytics configuration:', config);
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 excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING'];
const filteredConfigurations = configurations.filter(
config => !excludedCategories.includes(config.configCategory)
config => !excludedCategories.includes(config.configCategory) &&
!excludedConfigKeys.includes(config.configKey)
);
const groupedConfigs = filteredConfigurations.reduce((acc, config) => {

View File

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

View File

@ -88,6 +88,17 @@ export function HolidayManager() {
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 () => {
try {
setError(null);
@ -357,9 +368,12 @@ export function HolidayManager() {
type="date"
value={formData.holidayDate}
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"
/>
<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>
{/* Holiday Name Field */}

View File

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

View File

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

View File

@ -107,13 +107,10 @@ export function UserRoleManager() {
setSearching(true);
try {
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 }
// Axios response is in response.data, actual user array is in response.data.data
const users = response.data?.data || [];
console.log('Parsed users:', users);
setSearchResults(users);
} catch (error: any) {
@ -219,17 +216,11 @@ export function UserRoleManager() {
try {
const response = await userApi.getUsersByRole(roleFilter, page, limit);
console.log('Users response:', response);
// Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } }
const usersData = response.data?.data?.users || [];
const paginationData = response.data?.data?.pagination;
const summaryData = response.data?.data?.summary;
console.log('Parsed users:', usersData);
console.log('Pagination:', paginationData);
console.log('Summary:', summaryData);
setUsers(usersData);
if (paginationData) {
@ -257,11 +248,9 @@ export function UserRoleManager() {
const fetchRoleStatistics = async () => {
try {
const response = await userApi.getRoleStatistics();
console.log('Role statistics response:', response);
// Handle different response formats
const statsData = response.data?.data?.statistics || response.data?.statistics || [];
console.log('Statistics data:', statsData);
setRoleStats({
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"}
size="sm"
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}`}
aria-current={pageNum === currentPage ? 'page' : undefined}
>

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { LucideIcon } from 'lucide-react';
import { LucideIcon, Info } from 'lucide-react';
import { ReactNode } from 'react';
interface KPICardProps {
@ -12,6 +12,8 @@ interface KPICardProps {
children?: ReactNode;
testId?: string;
onClick?: () => void;
onJustifyClick?: () => void;
showJustifyButton?: boolean;
}
export function KPICard({
@ -23,8 +25,15 @@ export function KPICard({
subtitle,
children,
testId = 'kpi-card',
onClick
onClick,
onJustifyClick,
showJustifyButton = false
}: KPICardProps) {
const handleJustifyClick = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent card onClick from firing
onJustifyClick?.();
};
return (
<Card
className="hover:shadow-lg transition-all duration-200 shadow-md cursor-pointer h-full flex flex-col"
@ -38,11 +47,25 @@ export function KPICard({
>
{title}
</CardTitle>
<div className={`p-1.5 sm:p-2 rounded-lg ${iconBgColor}`} data-testid={`${testId}-icon-wrapper`}>
<Icon
className={`h-4 w-4 sm:h-4 sm:w-4 ${iconColor}`}
data-testid={`${testId}-icon`}
/>
<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`}>
<Icon
className={`h-4 w-4 sm:h-4 sm:w-4 ${iconColor}`}
data-testid={`${testId}-icon`}
/>
</div>
</div>
</CardHeader>
<CardContent className="flex flex-col flex-1 py-3">

View File

@ -112,7 +112,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
// Navigate to request detail page
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 || [];
setNotifications(notifs);
setUnreadCount(result.data?.unreadCount || 0);
console.log('[PageLayout] Loaded', notifs.length, 'recent notifications,', result.data?.unreadCount, 'unread');
} catch (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
const handleNewNotification = (data: { notification: Notification }) => {
console.log('[PageLayout] 🔔 New notification received:', data);
if (!mounted) return;
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>
<AlertDialogAction
onClick={async () => {
console.log('🔴 Logout button clicked in PageLayout');
console.log('🔴 onLogout function exists?', !!onLogout);
setShowLogoutDialog(false);
if (onLogout) {
console.log('🔴 Calling onLogout function...');
try {
await onLogout();
console.log('🔴 onLogout completed');
} catch (error) {
console.error('🔴 Error calling onLogout:', error);
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Clock, Lock, CheckCircle, XCircle, AlertTriangle, AlertOctagon } from 'lucide-react';
import { formatHoursMinutes } from '@/utils/slaTracker';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
export interface SLAData {
status: 'normal' | 'approaching' | 'critical' | 'breached';
@ -90,7 +91,7 @@ export function SLAProgressBar({
{sla.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>
)}

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useMemo } from 'react';
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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
@ -78,6 +78,7 @@ interface WorkNoteChatProps {
requestTitle?: string; // Optional title for display
onAttachmentsExtracted?: (attachments: any[]) => void; // Callback to pass attachments to parent
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
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`} />;
};
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 effectiveRequestId = requestId || routeParams.requestId || '';
const [message, setMessage] = useState('');
@ -193,6 +194,24 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
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
useEffect(() => {
// Participants state changed - logging removed
@ -230,7 +249,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
};
}) : [];
setMessages(mapped as any);
console.log(`[WorkNoteChat] Loaded ${mapped.length} messages from backend`);
} catch (error) {
console.error('[WorkNoteChat] Failed to load messages:', error);
}
@ -313,23 +331,19 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
useEffect(() => {
// Skip if participants are already loaded (prevents resetting on tab switch)
if (participantsLoadedRef.current) {
console.log('[WorkNoteChat] Participants already loaded, skipping reload');
return;
}
if (!effectiveRequestId) {
console.log('[WorkNoteChat] No requestId, skipping participants load');
return;
}
(async () => {
try {
console.log('[WorkNoteChat] Fetching participants from backend...');
const details = await getWorkflowDetails(effectiveRequestId);
const rows = Array.isArray(details?.participants) ? details.participants : [];
if (rows.length === 0) {
console.log('[WorkNoteChat] No participants found in backend response');
return;
}
@ -384,7 +398,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
return () => {
// Don't reset on unmount, only on request change
if (effectiveRequestId) {
console.log('[WorkNoteChat] Request changed, will reload participants on next mount');
participantsLoadedRef.current = false;
}
};
@ -408,7 +421,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
useEffect(() => {
const loadDocumentPolicy = async () => {
try {
const configs = await getAllConfigurations('DOCUMENT_POLICY');
const configs = await getPublicConfigurations('DOCUMENT_POLICY');
const configMap: Record<string, string> = {};
configs.forEach((c: AdminConfiguration) => {
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)
if (!skipSocketJoin) {
console.log('[WorkNoteChat] 🚪 About to join request room - requestId:', joinedId, 'userId:', currentUserId, 'socketId:', s.id);
joinRequestRoom(s, joinedId, currentUserId);
console.log('[WorkNoteChat] ✅ Emitted join:request event (standalone mode)');
// Mark self as online immediately after joining room
setParticipants(prev => {
const updated = prev.map(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;
});
} 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)
setParticipants(prev => {
const updated = prev.map(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;
});
}
// Handle new work notes
const noteHandler = (payload: any) => {
console.log('[WorkNoteChat] 📨 Received worknote:new event:', payload);
const n = payload?.note || payload;
if (!n) {
console.log('[WorkNoteChat] ⚠️ No note data in payload');
return;
}
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
setMessages(prev => {
if (prev.some(m => m.id === noteId)) {
console.log('[WorkNoteChat] ⏭️ Duplicate note, skipping:', noteId);
return prev; // Already exists, don't add
}
@ -525,66 +522,53 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
})) : undefined
} as any;
console.log('[WorkNoteChat] ✅ Adding new message to state:', newMessage.id);
return [...prev, newMessage];
});
};
// Handle presence: user joined
const presenceJoinHandler = (data: { userId: string; requestId: string }) => {
console.log('[WorkNoteChat] 🟢 presence:join received - userId:', data.userId, 'requestId:', data.requestId);
setParticipants(prev => {
if (prev.length === 0) {
console.log('[WorkNoteChat] ⚠️ Cannot update presence:join - no participants loaded yet');
return prev;
}
const participant = prev.find(p => (p as any).userId === data.userId);
if (!participant) {
console.log('[WorkNoteChat] ⚠️ User not found in participants list:', data.userId);
return prev;
}
const updated = prev.map(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;
});
};
// Handle presence: user left
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
if (data.userId === currentUserId) {
console.log('[WorkNoteChat] ⚠️ Ignoring presence:leave for self - staying online in own view');
return;
}
setParticipants(prev => {
if (prev.length === 0) {
console.log('[WorkNoteChat] ⚠️ Cannot update presence:leave - no participants loaded yet');
return prev;
}
const participant = prev.find(p => (p as any).userId === data.userId);
if (!participant) {
console.log('[WorkNoteChat] ⚠️ User not found in participants list:', data.userId);
return prev;
}
const updated = prev.map(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;
});
};
// Handle initial online users list
const presenceOnlineHandler = (data: { requestId: string; userIds: string[] }) => {
// Presence update received - logging removed
setParticipants(prev => {
if (prev.length === 0) {
console.log('[WorkNoteChat] ⚠️ No participants loaded yet, cannot update online status. Response will be ignored.');
return prev;
}
@ -608,36 +592,26 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
// Handle socket reconnection
const connectHandler = () => {
console.log('[WorkNoteChat] 🔌 Socket connected/reconnected');
// Mark self as online on connection
setParticipants(prev => {
const updated = prev.map(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;
});
// Rejoin room if needed
if (!skipSocketJoin) {
joinRequestRoom(s, joinedId, currentUserId);
console.log('[WorkNoteChat] 🔄 Rejoined request room on reconnection');
}
// Request online users on connection with multiple retries
if (participantsLoadedRef.current) {
console.log('[WorkNoteChat] 📡 Requesting online users after connection...');
s.emit('request:online-users', { requestId: joinedId });
// 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 }), 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
if (!skipSocketJoin) {
leaveRequestRoom(s, joinedId);
console.log('[WorkNoteChat] 🚪 Emitting leave:request for room (standalone mode)');
}
socketRef.current = null;
// Socket cleanup completed - logging removed
@ -729,7 +702,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
const participant = participants.find(p =>
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;
})
.filter(Boolean);
@ -878,20 +850,13 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
(async () => {
try {
const rows = await getWorkNotes(effectiveRequestId);
console.log('[WorkNoteChat] Loaded work notes from backend:', rows);
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
const userName = m.userName || m.user_name || 'User';
const userRole = m.userRole || m.user_role; // Get role directly from backend
const participantRole = getFormattedRole(userRole);
const noteUserId = m.userId || m.user_id;
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
const userName = m.userName || m.user_name || 'User';
const userRole = m.userRole || m.user_role; // Get role directly from backend
const participantRole = getFormattedRole(userRole);
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()),
user: {
name: userName,
@ -1036,7 +1001,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
// Request updated online users list from server to get correct status
if (socketRef.current && socketRef.current.connected) {
console.log('[WorkNoteChat] 📡 Requesting online users after adding spectator...');
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;
};
@ -1745,40 +1708,43 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
</div>
</div>
<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>
<div className="space-y-2">
{/* Only initiator can add approvers */}
{isInitiator && (
{/* Quick Actions Section - Hide for spectators */}
{!effectiveIsSpectator && (
<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>
<div className="space-y-2">
{/* Only initiator can add approvers */}
{isInitiator && (
<Button
variant="outline"
size="sm"
className="w-full justify-start gap-2 h-9 text-sm"
onClick={() => setShowAddApproverModal(true)}
>
<UserPlus className="h-4 w-4" />
Add Approver
</Button>
)}
<Button
variant="outline"
size="sm"
className="w-full justify-start gap-2 h-9 text-sm"
onClick={() => setShowAddApproverModal(true)}
onClick={() => setShowAddSpectatorModal(true)}
>
<UserPlus className="h-4 w-4" />
Add Approver
<Eye className="h-4 w-4" />
Add Spectator
</Button>
)}
<Button
variant="outline"
size="sm"
className="w-full justify-start gap-2 h-9 text-sm"
onClick={() => setShowAddSpectatorModal(true)}
>
<Eye className="h-4 w-4" />
Add Spectator
</Button>
{/* <Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Bell className="h-4 w-4" />
Manage Notifications
</Button>
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Archive className="h-4 w-4" />
Archive Chat
</Button> */}
{/* <Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Bell className="h-4 w-4" />
Manage Notifications
</Button>
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Archive className="h-4 w-4" />
Archive Chat
</Button> */}
</div>
</div>
</div>
)}
</div>
</div>
@ -1796,18 +1762,20 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
/>
)}
{/* Add Spectator Modal */}
<AddSpectatorModal
open={showAddSpectatorModal}
onClose={() => setShowAddSpectatorModal(false)}
onConfirm={handleAddSpectator}
requestIdDisplay={effectiveRequestId}
requestTitle={requestInfo.title}
existingParticipants={existingParticipants}
/>
{/* Add Spectator Modal - Hide for spectators */}
{!effectiveIsSpectator && (
<AddSpectatorModal
open={showAddSpectatorModal}
onClose={() => setShowAddSpectatorModal(false)}
onConfirm={handleAddSpectator}
requestIdDisplay={effectiveRequestId}
requestTitle={requestInfo.title}
existingParticipants={existingParticipants}
/>
)}
{/* Add Approver Modal */}
{isInitiator && (
{/* Add Approver Modal - Hide for spectators */}
{!effectiveIsSpectator && isInitiator && (
<AddApproverModal
open={showAddApproverModal}
onClose={() => setShowAddApproverModal(false)}

View File

@ -150,11 +150,8 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
const n = payload?.note || payload;
if (!n) return;
console.log('[WorkNoteChat] Received note via socket:', n);
setMessages(prev => {
if (prev.some(m => m.id === (n.noteId || n.note_id || n.id))) {
console.log('[WorkNoteChat] Duplicate detected, skipping');
return prev;
}
@ -175,7 +172,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
isCurrentUser: noteUserId === currentUserId
} as any;
console.log('[WorkNoteChat] Adding new message:', newMsg);
return [...prev, newMsg];
});
};
@ -265,7 +261,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
(async () => {
try {
const rows = await getWorkNotes(requestId);
console.log('[WorkNoteChat] Loaded work notes:', rows);
const mapped = Array.isArray(rows) ? rows.map((m: any) => {
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 { Textarea } from '@/components/ui/textarea';
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 { useAuth, hasManagementAccess } from '@/contexts/AuthContext';
import { updateBreachReason as updateBreachReasonApi } from '@/services/workflowApi';
@ -339,7 +339,7 @@ export function ApprovalStepCard({
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Due by:</span>
<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>
</div>
@ -532,13 +532,13 @@ export function ApprovalStepCard({
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Allocated:</span>
<span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursAllocated || 0).toFixed(2)}h
{formatHoursMinutes(Number(alert.tatHoursAllocated || 0))}
</span>
</div>
<div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Elapsed:</span>
<span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursElapsed || 0).toFixed(2)}h
{formatHoursMinutes(Number(alert.tatHoursElapsed || 0))}
{alert.metadata?.tatTestMode && (
<span className="text-purple-600 ml-1">
({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m)
@ -551,7 +551,7 @@ export function ApprovalStepCard({
<span className={`ml-1 font-medium ${
(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 && (
<span className="text-purple-600 ml-1">
({(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">
<span className="text-gray-500">Due by:</span>
<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>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { Progress } from '@/components/ui/progress';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
import {
ArrowLeft,
Clock,
@ -261,7 +262,7 @@ export function ClaimManagementDetail({
</div>
<Progress value={claim.slaProgress} className="h-2 mb-2" />
<p className="text-xs text-gray-600">
Due: {claim.slaEndDate} {claim.slaProgress}% elapsed
Due: {formatDateDDMMYYYY(claim.slaEndDate, true)} {claim.slaProgress}% elapsed
</p>
</div>
</div>
@ -762,8 +763,7 @@ export function ClaimManagementDetail({
<DealerDocumentModal
isOpen={dealerDocModal}
onClose={() => setDealerDocModal(false)}
onSubmit={async (documents) => {
console.log('Dealer documents submitted:', documents);
onSubmit={async (_documents) => {
toast.success('Documents Uploaded', {
description: 'Your documents have been submitted for review.',
});
@ -779,7 +779,6 @@ export function ClaimManagementDetail({
isOpen={initiatorVerificationModal}
onClose={() => setInitiatorVerificationModal(false)}
onSubmit={async (data) => {
console.log('Verification data:', data);
toast.success('Verification Complete', {
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
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 { useMultiUserSearch } from '@/hooks/useUserSearch';
import { useAuth } from '@/contexts/AuthContext';
import { ensureUserExists } from '@/services/userApi';
interface ApprovalWorkflowStepProps {
@ -35,7 +34,6 @@ export function ApprovalWorkflowStep({
updateFormData,
onValidationError
}: ApprovalWorkflowStepProps) {
const { user } = useAuth();
const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch();
const handleApproverEmailChange = (index: number, value: string) => {

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
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 { FormData } from '@/hooks/useCreateRequestForm';
import { useUserSearch } from '@/hooks/useUserSearch';
import { ensureUserExists } from '@/services/userApi';
interface ParticipantsStepProps {
formData: FormData;

View File

@ -3,7 +3,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
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';
interface ReviewSubmitStepProps {

View File

@ -1,5 +1,5 @@
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 { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';

View File

@ -74,9 +74,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const forceLogout = sessionStorage.getItem('__force_logout__');
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
sessionStorage.removeItem('__logout_in_progress__');
sessionStorage.removeItem('__force_logout__');
@ -98,17 +95,12 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
setIsLoading(false);
setError(null);
console.log('🔴 Logout complete - user should see login screen');
return;
}
// PRIORITY 2: Check if URL has logout parameter (from redirect)
const urlParams = new URLSearchParams(window.location.search);
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();
localStorage.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 (!hasAuthData) {
console.log('🔴 No auth data found - setting unauthenticated state');
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
@ -144,7 +135,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
if (!isLoggingOut) {
checkAuthStatus();
} else {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false);
}
}, [isLoggingOut]);
@ -211,12 +201,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
// This is the frontend callback URL, NOT the backend URL
// Backend will use this same URI when exchanging code with Okta
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);
@ -244,7 +228,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const checkAuthStatus = async () => {
// Don't check auth status if we're in the middle of logging out
if (isLoggingOut) {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false);
return;
}
@ -255,11 +238,8 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const token = TokenManager.getAccessToken();
const storedUser = TokenManager.getUserData();
console.log('🔍 Checking auth status:', { hasToken: !!token, hasUser: !!storedUser, isLoggingOut });
// If no token at all, user is not authenticated
if (!token) {
console.log('🔍 No token found - setting unauthenticated');
setIsAuthenticated(false);
setUser(null);
setIsLoading(false);
@ -361,7 +341,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
// This ensures Okta requires login even if a session still exists
if (isAfterLogout) {
authUrl += `&prompt=login`;
console.log('🔐 Adding prompt=login to force re-authentication after logout');
}
window.location.href = authUrl;
@ -372,13 +351,11 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
};
const logout = async () => {
console.log('🚪 LOGOUT FUNCTION CALLED - Starting logout process');
console.log('🚪 Current auth state:', { isAuthenticated, hasUser: !!user, isLoading });
try {
// CRITICAL: Get id_token from TokenManager before clearing anything
// 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
// This must be set BEFORE clearing storage so it survives
@ -386,20 +363,16 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
sessionStorage.setItem('__force_logout__', 'true');
setIsLoggingOut(true);
console.log('🚪 Step 1: Resetting auth state...');
// Reset auth state FIRST to prevent any re-authentication
setIsAuthenticated(false);
setUser(null);
setError(null);
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
// IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies
try {
console.log('🚪 Step 2: Calling backend logout API to clear httpOnly cookies...');
await logoutApi();
console.log('🚪 Step 2: Backend logout API completed - httpOnly cookies should be cleared');
} catch (err) {
console.error('🚪 Logout API error:', err);
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)
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
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 (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
await new Promise(resolve => setTimeout(resolve, 100));
@ -523,11 +474,8 @@ export function _Auth0AuthProvider({ children }: { children: ReactNode }) {
authorizationParams={{
redirect_uri: window.location.origin + '/login/callback',
}}
onRedirectCallback={(appState) => {
console.log('Auth0 Redirect Callback:', {
appState,
returnTo: appState?.returnTo || window.location.pathname,
});
onRedirectCallback={(_appState) => {
// Auth0 redirect callback handled
}}
>
<Auth0ContextWrapper>{children}</Auth0ContextWrapper>

View File

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

View File

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

View File

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

View File

@ -237,8 +237,10 @@ export function useRequestDetails(
},
createdAt: wf.createdAt,
updatedAt: wf.updatedAt,
totalSteps: wf.totalLevels,
currentStep: summary?.currentLevel || wf.currentLevel,
totalSteps: wf.totalLevels || 1,
// 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,
conclusionRemark: wf.conclusionRemark || null,
closureDate: wf.closureDate || null,
@ -407,8 +409,10 @@ export function useRequestDetails(
},
createdAt: wf.createdAt,
updatedAt: wf.updatedAt,
totalSteps: wf.totalLevels,
currentStep: summary?.currentLevel || wf.currentLevel,
totalSteps: wf.totalLevels || 1,
// 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,
approvals,
documents: mappedDocuments,

View File

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

View File

@ -4,13 +4,6 @@ import { AuthProvider } from './contexts/AuthContext';
import { AuthenticatedApp } from './pages/Auth';
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(
<React.StrictMode>
<AuthProvider>

View File

@ -2,7 +2,7 @@
* 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 type { ApproverPerformance } from '@/services/dashboard.service';
import type { ApproverPerformanceStats } from '../types/approverPerformance.types';
@ -33,7 +33,7 @@ export function ApproverPerformanceActionsStats({
<Users className="w-4 h-4" />
Approver's Actions
</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="flex items-center justify-between mb-2">
<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-xs text-gray-600 mt-1">Pending Actions</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="flex items-center justify-between mb-2">
<FileText className="w-5 h-5 text-blue-600" />

View File

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

View File

@ -25,6 +25,13 @@ export function calculateApproverStats(
return ['pending', 'in_progress', 'in-progress'].includes(status);
}).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'
// This includes pending requests that have breached their TAT
const breached = filtered.filter(r => {
@ -72,6 +79,7 @@ export function calculateApproverStats(
approvedByApprover,
rejectedByApprover,
pendingByApprover,
closedByApprover,
// TAT stats
breached,
compliant,

View File

@ -6,28 +6,13 @@ import { LogIn, Shield } from 'lucide-react';
export function Auth() {
const { login, isLoading, error } = useAuth();
console.log('Auth Component Render - Auth0 State:', {
isLoading,
error: error?.message,
timestamp: new Date().toISOString()
});
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
console.log('Clearing local storage and session storage...');
localStorage.clear();
sessionStorage.clear();
console.log('Storage cleared');
try {
await login();
console.log('Login redirect initiated successfully');
} catch (loginError) {
console.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 handleLogout = async () => {
console.log('🔵 ========================================');
console.log('🔵 AuthenticatedApp.handleLogout - CALLED');
console.log('🔵 Timestamp:', new Date().toISOString());
console.log('🔵 logout function exists?', !!logout);
console.log('🔵 ========================================');
try {
if (!logout) {
console.error('🔵 ERROR: logout function is undefined!');
return;
}
console.log('🔵 Calling logout from auth context...');
// Call logout from auth context (handles all cleanup and redirect)
await logout();
console.log('🔵 Logout successful - redirecting to login...');
} catch (logoutError) {
console.error('🔵 Logout error in handleLogout:', logoutError);
// Even if logout fails, clear local data and redirect
try {
console.log('🔵 Attempting emergency cleanup...');
localStorage.clear();
sessionStorage.clear();
window.location.href = '/';
@ -44,33 +35,7 @@ export function AuthenticatedApp() {
};
useEffect(() => {
console.log('AuthenticatedApp - 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('========================================');
}
// Auth state changed
}, [isAuthenticated, isLoading, error, user]);
// Always show callback loader when on callback route (after all hooks)
@ -80,7 +45,6 @@ export function AuthenticatedApp() {
// Show loading state while checking authentication
if (isLoading) {
console.log('Auth0 is still loading...');
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="text-center">
@ -112,12 +76,10 @@ export function AuthenticatedApp() {
// Show login screen if not authenticated
if (!isAuthenticated) {
console.log('User not authenticated, showing login screen');
return <Auth />;
}
// User is authenticated, show the app
console.log('User authenticated successfully, showing main application');
return (
<>
<App onLogout={handleLogout} />

View File

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

View File

@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowRight, Calendar, CheckCircle } from 'lucide-react';
import { formatDateTime } from '@/utils/dateFormatter';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { ClosedRequest } from '../types/closedRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
@ -88,13 +88,13 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
<div className="flex items-center gap-1.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>
{request.dueDate && (
<div className="flex items-center gap-1.5">
<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>

View File

@ -110,12 +110,6 @@ export function ClosedRequestsFilters({
</SelectTrigger>
<SelectContent>
<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">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-gray-600" />

View File

@ -66,7 +66,7 @@ export function ClosedRequestsPagination({
variant={pageNum === currentPage ? "default" : "outline"}
size="sm"
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}`}
>
{pageNum}

View File

@ -41,11 +41,28 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
// Always use user-scoped endpoint (not organization-wide)
// Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user)
// 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({
page,
limit: itemsPerPage,
search: filters?.search,
status: filters?.status,
status: statusFilter && statusFilter !== 'all' ? statusFilter : undefined,
priority: filters?.priority,
sortBy: filters?.sortBy,
sortOrder: filters?.sortOrder
@ -56,9 +73,23 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
? (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
// 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;
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({
currentPage: paginationData.page || 1,
totalPages: paginationData.totalPages || 1,
@ -66,9 +97,6 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
itemsPerPage,
});
}
const mapped = transformClosedRequests(data);
setRequests(mapped);
} catch (error) {
console.error('[ClosedRequests] Error fetching requests:', error);
setRequests([]);

View File

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

View File

@ -30,14 +30,6 @@ export function getPriorityConfig(priority: string): PriorityConfig {
export function getStatusConfig(status: string): StatusConfig {
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':
return {
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,
title: r.title,
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',
initiator: {
name: r.initiator?.displayName || r.initiator?.email || '—',

View File

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

View File

@ -157,6 +157,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
dateRange?: DateRange;
startDate?: Date;
endDate?: Date;
targetPage?: 'requests' | 'open-requests' | 'my-requests';
}) => {
const url = buildFilterUrl(filters);
onNavigate?.(url);
@ -211,11 +212,14 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
customStartDate={customStartDate}
customEndDate={customEndDate}
onKPIClick={handleKPIClick}
onNavigate={onNavigate}
userId={(user as any)?.userId}
userDisplayName={(user as any)?.displayName || (user as any)?.email}
/>
)}
{/* 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
isAdmin={isAdmin}
breachedRequests={breachedRequests}

View File

@ -8,7 +8,8 @@ import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Activity, MessageSquare, PieChart, Download } from 'lucide-react';
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 { KPIClickFilters } from '../../components/types/dashboard.types';

View File

@ -144,7 +144,7 @@ export function AdminKPICards({
{/* Avg Cycle Time */}
<KPICard
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}
iconBgColor="bg-purple-50"
iconColor="text-purple-600"
@ -152,37 +152,39 @@ export function AdminKPICards({
testId="kpi-avg-cycle-time"
onClick={() => onKPIClick(getFilterParams())}
>
<div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard
label="Express"
value={(() => {
const express = priorityDistribution.find((p) => p.priority === 'express');
const hours = express ? Number(express.avgCycleTimeHours) : 0;
return hours > 0 ? formatHoursMinutes(hours) : 'N/A';
})()}
bgColor="bg-orange-50"
textColor="text-orange-600"
testId="stat-express-time"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), priority: 'express' });
}}
/>
<StatCard
label="Standard"
value={(() => {
const standard = priorityDistribution.find((p) => p.priority === 'standard');
const hours = standard ? Number(standard.avgCycleTimeHours) : 0;
return hours > 0 ? formatHoursMinutes(hours) : 'N/A';
})()}
bgColor="bg-blue-50"
textColor="text-blue-600"
testId="stat-standard-time"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), priority: 'standard' });
}}
/>
<div className="flex flex-col flex-1">
<div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard
label="Express"
value={(() => {
const express = priorityDistribution.find((p) => p.priority === 'express');
const hours = express ? Number(express.avgCycleTimeHours) : 0;
return hours > 0 ? formatHoursMinutes(hours) : 'N/A';
})()}
bgColor="bg-orange-50"
textColor="text-orange-600"
testId="stat-express-time"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), priority: 'express' });
}}
/>
<StatCard
label="Standard"
value={(() => {
const standard = priorityDistribution.find((p) => p.priority === 'standard');
const hours = standard ? Number(standard.avgCycleTimeHours) : 0;
return hours > 0 ? formatHoursMinutes(hours) : 'N/A';
})()}
bgColor="bg-blue-50"
textColor="text-blue-600"
testId="stat-standard-time"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), priority: 'standard' });
}}
/>
</div>
</div>
</KPICard>
</div>

View File

@ -8,7 +8,8 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Flame, CheckCircle, ArrowRight } from 'lucide-react';
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';
interface CriticalAlertsSectionProps {

View File

@ -124,7 +124,7 @@ export function RecentActivitySection({
variant={pageNum === pagination.page ? 'default' : 'outline'}
size="sm"
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}`}
>
{pageNum}

View File

@ -6,7 +6,8 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
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 { formatBreachTime } from '../../utils/dashboardCalculations';
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 { Progress } from '@/components/ui/progress';
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';
interface UserKPICardsProps {
@ -18,6 +19,9 @@ interface UserKPICardsProps {
customStartDate?: Date;
customEndDate?: Date;
onKPIClick: (filters: KPIClickFilters) => void;
onNavigate?: (page: string) => void;
userId?: string;
userDisplayName?: string;
}
export function UserKPICards({
@ -27,6 +31,9 @@ export function UserKPICards({
customStartDate,
customEndDate,
onKPIClick,
onNavigate,
userId,
userDisplayName,
}: UserKPICardsProps) {
const getFilterParams = () => ({
dateRange,
@ -34,6 +41,19 @@ export function UserKPICards({
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
? ((kpis.requestVolume.approvedRequests / kpis.requestVolume.totalRequests) * 100)
: 0;
@ -74,14 +94,14 @@ export function UserKPICards({
}}
/>
<StatCard
label="Draft"
value={kpis?.requestVolume.draftRequests || 0}
bgColor="bg-gray-50"
textColor="text-gray-600"
testId="stat-user-draft"
label="Rejected"
value={kpis?.requestVolume.rejectedRequests || 0}
bgColor="bg-red-50"
textColor="text-red-600"
testId="stat-user-rejected"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), status: 'draft' });
onKPIClick({ ...getFilterParams(), status: 'rejected' });
}}
/>
<StatCard
@ -107,6 +127,9 @@ export function UserKPICards({
iconColor="text-orange-600"
subtitle="at current level"
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">
<StatCard
@ -115,6 +138,10 @@ export function UserKPICards({
bgColor="bg-blue-50"
textColor="text-blue-600"
testId="stat-today"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' });
}}
/>
<StatCard
label="This Week"
@ -122,6 +149,10 @@ export function UserKPICards({
bgColor="bg-green-50"
textColor="text-green-600"
testId="stat-week"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' });
}}
/>
</div>
</KPICard>
@ -134,6 +165,9 @@ export function UserKPICards({
iconBgColor="bg-red-50"
iconColor="text-red-600"
testId="kpi-user-critical"
onClick={() => onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' })}
onJustifyClick={handleNavigateToApproverPerformance}
showJustifyButton={!!userId}
>
<div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard
@ -142,6 +176,10 @@ export function UserKPICards({
bgColor="bg-orange-50"
textColor="text-red-600"
testId="stat-user-breached"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' });
}}
/>
<StatCard
label="Warning"
@ -149,6 +187,10 @@ export function UserKPICards({
bgColor="bg-yellow-50"
textColor="text-orange-600"
testId="stat-user-warning"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' });
}}
/>
</div>
</KPICard>

View File

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

View File

@ -4,7 +4,8 @@
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 { ActivityData, CriticalAlertData } from '@/components/dashboard/ActivityFeedItem';
import { ActivityData } from '@/components/dashboard/ActivityFeedItem';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
interface UseDashboardDataOptions {
isAdmin: boolean;

View File

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

View File

@ -41,17 +41,25 @@ export function getUpcomingDeadlinesNotBreached(
/**
* 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 {
if (hours <= 0) return 'Just breached';
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 m = Math.round((hours - h) * 60);
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) {
const h = Math.floor(remainingHours);
const m = Math.round((remainingHours - h) * 60);

View File

@ -16,6 +16,7 @@ export function buildFilterUrl(filters: {
dateRange?: DateRange;
startDate?: Date;
endDate?: Date;
targetPage?: 'requests' | 'open-requests' | 'my-requests';
}): string {
const params = new URLSearchParams();
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.endDate) params.set('endDate', filters.endDate.toISOString());
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 {

View File

@ -68,7 +68,7 @@ export function RequestLifecycleReport({
</div>
<div>
<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>
<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 { formatTAT, formatDate, formatDateForCSV, mapActivityType } from './formatting';
import { LifecycleRequest, ActivityLogEntry, AgingWorkflow } from '../types/detailedReports.types';
import { getWorkflowDetails } from '@/services/workflowApi';
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(
dateRange: DateRange,
@ -17,42 +18,198 @@ export async function exportLifecycleToCSV(
// Fetch all data with a very large limit
const result = await dashboardService.getLifecycleReport(1, 10000, dateRange, customStartDate, customEndDate);
// CSV Headers for comprehensive end-to-end workflow status
const csvRows = [
[
'Request Number',
'Title',
'Priority',
'Status',
'Overall Status',
'Initiator',
'Submission Date',
'Current Stage',
'Overall TAT',
'Current Level',
'Closure Date',
'Total Levels',
'Current Level',
'Overall TAT (Hours)',
'Overall TAT (Days)',
'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(','),
];
result.lifecycleData.forEach((req: any) => {
const overallTAT = formatTAT(req.overallTATHours);
const submissionDateCSV = req.submissionDate ? formatDateForCSV(req.submissionDate) : 'N/A';
const row = [
req.requestNumber || '',
`"${(req.title || '').replace(/"/g, '""')}"`,
req.priority || 'medium',
req.status || '',
`"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`,
submissionDateCSV,
`"${(req.currentStageName || `Level ${req.currentLevel}`).replace(/"/g, '""')}"`,
overallTAT,
(req.currentLevel || '').toString(),
(req.totalLevels || '').toString(),
(req.breachCount || 0).toString(),
];
csvRows.push(row.join(','));
});
// Process each request and fetch detailed approval level information
for (const req of result.lifecycleData) {
try {
// Fetch detailed workflow information including all approval levels
const workflowDetails = await getWorkflowDetails(req.requestId || req.requestNumber);
downloadCSV(csvRows, `lifecycle-report-${new Date().toISOString().split('T')[0]}.csv`);
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 = [
req.requestNumber || '',
`"${(req.title || '').replace(/"/g, '""')}"`,
req.priority || 'medium',
req.status || '',
`"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`,
submissionDateCSV,
closureDateCSV,
totalLevels.toString(),
currentLevel.toString(),
overallTATHours.toString(),
overallTATDays,
(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(','));
});
}
} 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';
// 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 { MyRequest } from '../types/myRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface RequestCardProps {
request: MyRequest;
@ -81,7 +82,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
</span>
<span className="truncate" data-testid="submitted-date">
<span className="font-medium">Submitted:</span>{' '}
{request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}
{formatDateDDMMYYYY(request.submittedDate)}
</span>
</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">
<Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span data-testid="submitted-timestamp">
Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}
Submitted: {formatDateDDMMYYYY(request.submittedDate)}
</span>
</div>
</div>

View File

@ -47,8 +47,6 @@ export function Notifications({ onNavigate }: NotificationsProps) {
setNotifications(notifs);
setTotalCount(total);
setTotalPages(Math.ceil(total / ITEMS_PER_PAGE));
console.log(`[Notifications] Loaded page ${page}, ${notifs.length} notifications, ${total} total`);
} catch (error) {
console.error('[Notifications] Failed to fetch:', error);
} 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 { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@ -6,9 +7,9 @@ import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
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 { formatDateShort } from '@/utils/dateFormatter';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface Request {
id: string;
title: string;
@ -98,12 +99,18 @@ const getStatusConfig = (status: string) => {
// getSLAUrgency removed - now using SLATracker component for real-time SLA display
export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [searchTerm, setSearchTerm] = useState('');
const [priorityFilter, setPriorityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>('created');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [searchParams] = useSearchParams();
// Initialize filters from URL params
const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all');
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 [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
@ -140,15 +147,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
sortBy: filters?.sortBy,
sortOrder: filters?.sortOrder
});
console.log('[OpenRequests] API Response:', result); // Debug log
// Extract data - workflowApi now returns { data: [], pagination: {} }
const data = Array.isArray((result as any)?.data)
? (result as any).data
: [];
console.log('[OpenRequests] Parsed data count:', data.length); // Debug log
// Set pagination data
const pagination = (result as any)?.pagination;
if (pagination) {
@ -231,31 +235,39 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
return pages;
};
// Initial fetch on mount
// Track if this is the initial mount
const isInitialMount = useRef(true);
// Initial fetch on mount with URL params
useEffect(() => {
fetchRequests(1, {
search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined,
sortBy,
sortOrder
});
}, [fetchRequests]);
if (isInitialMount.current) {
isInitialMount.current = false;
fetchRequests(1, {
search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined,
sortBy,
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(() => {
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, {
search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined,
sortBy,
sortOrder
});
}
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
return () => clearTimeout(timeoutId);
@ -330,28 +342,17 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</CardDescription>
</div>
</div>
<div className="flex items-center gap-1 sm:gap-2">
{activeFiltersCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-red-600 hover:bg-red-50 gap-1 h-8 sm:h-9 px-2 sm:px-3"
>
<X className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
<span className="text-xs sm:text-sm">Clear</span>
</Button>
)}
{activeFiltersCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="gap-1 sm:gap-2 h-8 sm:h-9 px-2 sm:px-3"
onClick={clearFilters}
className="text-red-600 hover:bg-red-50 gap-1 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>
<X className="w-3 h-3 sm:w-3.5 sm:h-3.5" />
<span className="text-xs sm:text-sm">Clear</span>
</Button>
</div>
)}
</div>
</CardHeader>
<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">
<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>
@ -621,7 +622,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
variant={pageNum === currentPage ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(pageNum)}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
>
{pageNum}
</Button>

View File

@ -165,7 +165,6 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab');
if (tabParam) {
console.log('[RequestDetail] Auto-switching to tab:', tabParam);
setActiveTab(tabParam);
}
}, [requestIdentifier]);
@ -227,47 +226,47 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
{/* 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">
<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">
<div className="mb-4 sm:mb-6">
<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
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"
>
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Overview</span>
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Overview</span>
</TabsTrigger>
<TabsTrigger
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"
>
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Workflow</span>
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Workflow</span>
</TabsTrigger>
<TabsTrigger
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"
>
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Docs</span>
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Docs</span>
</TabsTrigger>
<TabsTrigger
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"
>
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Activity</span>
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Activity</span>
</TabsTrigger>
<TabsTrigger
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"
>
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span>Work Notes</span>
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="truncate">Work Notes</span>
{unreadWorkNotes > 0 && (
<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"
@ -339,6 +338,7 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
mergedMessages={mergedMessages}
setWorkNoteAttachments={setWorkNoteAttachments}
isInitiator={isInitiator}
isSpectator={isSpectator}
currentLevels={currentLevels}
onAddApprover={handleAddApprover}
/>

View File

@ -30,64 +30,66 @@ export function QuickActionsSidebar({
}: QuickActionsSidebarProps) {
return (
<div className="space-y-4 sm:space-y-6">
{/* Quick Actions Card */}
<Card data-testid="quick-actions-card">
<CardHeader className="pb-2">
<CardTitle className="text-sm sm:text-base">Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{/* Add Approver */}
{isInitiator && request.status !== 'closed' && (
<Button
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"
onClick={onAddApprover}
data-testid="add-approver-button"
>
<UserPlus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Add Approver
</Button>
)}
{/* Add Spectator */}
{!isSpectator && request.status !== 'closed' && (
<Button
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"
onClick={onAddSpectator}
data-testid="add-spectator-button"
>
<Eye className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Add Spectator
</Button>
)}
{/* Approve/Reject Buttons */}
<div className="pt-3 sm:pt-4 space-y-2">
{!isSpectator && currentApprovalLevel && (
<>
<Button
className="w-full bg-green-600 hover:bg-green-700 text-white h-9 sm:h-10 text-xs sm:text-sm"
onClick={onApprove}
data-testid="approve-request-button"
>
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
Approve Request
</Button>
<Button
variant="destructive"
className="w-full h-9 sm:h-10 text-xs sm:text-sm"
onClick={onReject}
data-testid="reject-request-button"
>
<XCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
Reject Request
</Button>
</>
{/* Quick Actions Card - Hide entire card for spectators */}
{!isSpectator && (
<Card data-testid="quick-actions-card">
<CardHeader className="pb-2">
<CardTitle className="text-sm sm:text-base">Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{/* Add Approver */}
{isInitiator && request.status !== 'closed' && (
<Button
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"
onClick={onAddApprover}
data-testid="add-approver-button"
>
<UserPlus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Add Approver
</Button>
)}
</div>
</CardContent>
</Card>
{/* Add Spectator */}
{request.status !== 'closed' && (
<Button
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"
onClick={onAddSpectator}
data-testid="add-spectator-button"
>
<Eye className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
Add Spectator
</Button>
)}
{/* Approve/Reject Buttons */}
<div className="pt-3 sm:pt-4 space-y-2">
{currentApprovalLevel && (
<>
<Button
className="w-full bg-green-600 hover:bg-green-700 text-white h-9 sm:h-10 text-xs sm:text-sm"
onClick={onApprove}
data-testid="approve-request-button"
>
<CheckCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
Approve Request
</Button>
<Button
variant="destructive"
className="w-full h-9 sm:h-10 text-xs sm:text-sm"
onClick={onReject}
data-testid="reject-request-button"
>
<XCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
Reject Request
</Button>
</>
)}
</div>
</CardContent>
</Card>
)}
{/* Spectators Card */}
{request.spectators && request.spectators.length > 0 && (

View File

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

View File

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

View File

@ -30,10 +30,51 @@ export function WorkflowTab({ request, user, isInitiator, onSkipApprover, onRefr
</CardDescription>
</div>
{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 (
<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>
);
})()}

View File

@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { User, ArrowRight, TrendingUp, Clock } from 'lucide-react';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
import type { ConvertedRequest } from '../types/requests.types';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface RequestCardProps {
request: ConvertedRequest;
@ -81,7 +82,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<span className="font-medium">ID:</span> {request.displayId || request.id}
</span>
<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>
</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">
<Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span data-testid="submitted-timestamp">
Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}
Submitted: {formatDateDDMMYYYY(request.submittedDate)}
</span>
</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[]> => {
const params = category ? { category } : {};

View File

@ -100,12 +100,6 @@ export async function exchangeCodeForTokens(
code: string,
redirectUri: string
): Promise<TokenExchangeResponse> {
console.log('🔄 Exchange Code for Tokens:', {
code: code ? `${code.substring(0, 10)}...` : 'MISSING',
redirectUri,
endpoint: `${API_BASE_URL}/auth/token-exchange`,
});
try {
const response = await apiClient.post<TokenExchangeResponse>(
'/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)
if (Array.isArray(response.data)) {
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)
if (result.idToken) {
TokenManager.setIdToken(result.idToken);
console.log('✅ ID token stored for logout');
}
console.log('✅ Tokens stored successfully');
} else {
console.warn('⚠️ Tokens missing in response', { result });
}
@ -219,13 +196,10 @@ export async function getCurrentUser() {
*/
export async function logout(): Promise<void> {
try {
console.log('📡 Calling backend logout endpoint to clear httpOnly cookies...');
// 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
});
console.log('📡 Backend logout response:', response.status, response.statusText);
console.log('📡 Response headers (check Set-Cookie):', response.headers);
} catch (error: any) {
console.error('📡 Logout API error:', error);
console.error('📡 Error details:', {

View File

@ -141,8 +141,6 @@ class ConfigService {
const response = await apiClient.get('/config');
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)
return {
...DEFAULT_CONFIG,

View File

@ -693,6 +693,16 @@
scrollbar-width: thin;
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 {

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_END_DAY = config.workingHours.END_DAY;
configLoaded = true;
console.log('[SLA Tracker] ✅ Loaded working hours from backend:', { WORK_START_HOUR, WORK_END_HOUR });
} catch (error) {
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")
* Simple format without considering working days
* Format decimal hours to hours and minutes format
* 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 {
if (hours === null || hours === undefined || hours < 0) return '0h';
if (hours === 0) return '0h';
if (hours === null || hours === undefined || hours < 0) return '0 hours';
if (hours === 0) return '0 hours';
const totalMinutes = Math.round(hours * 60);
const h = Math.floor(totalMinutes / 60);
const m = totalMinutes % 60;
const WORKING_HOURS_PER_DAY = 8;
if (h > 0 && m > 0) {
return `${h}h ${m}m`;
} else if (h > 0) {
return `${h}h`;
// If less than 1 hour, show minutes only
if (hours < 1) {
const m = Math.round(hours * 60);
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 {
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 `${m}m`;
return `${remainingHrs} ${hourLabel}`;
}
}

View File

@ -29,8 +29,6 @@ export function getSocket(baseUrl?: string): Socket {
const url = baseUrl || getSocketBaseUrl();
if (socket) return socket;
console.log('[Socket] Connecting to:', url);
socket = io(url, {
withCredentials: true,
transports: ['websocket', 'polling'],
@ -41,15 +39,15 @@ export function getSocket(baseUrl?: string): Socket {
});
socket.on('connect', () => {
console.log('[Socket] Connected successfully:', socket?.id);
// Socket connected
});
socket.on('connect_error', (error) => {
console.error('[Socket] Connection error:', error.message);
});
socket.on('disconnect', (reason) => {
console.log('[Socket] Disconnected:', reason);
socket.on('disconnect', (_reason) => {
// Socket disconnected
});
return socket;
@ -69,7 +67,6 @@ export function leaveRequestRoom(socket: Socket, requestId: string) {
export function joinUserRoom(socket: Socket, userId: string) {
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
*/
static clearAll(): void {
console.log('TokenManager.clearAll() - Starting cleanup...');
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
// This flag survives the redirect and prevents auto-authentication
try {
@ -212,7 +210,6 @@ export class TokenManager {
try {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
console.log(`Removed ${key} from storage`);
} catch (e) {
console.warn(`Error removing ${key}:`, e);
}
@ -233,7 +230,6 @@ export class TokenManager {
allLocalStorageKeys.forEach(key => {
try {
localStorage.removeItem(key);
console.log(`Removed localStorage key: ${key}`);
} catch (e) {
console.warn(`Error removing localStorage key ${key}:`, e);
}
@ -241,7 +237,6 @@ export class TokenManager {
// Final clear as backup
localStorage.clear();
console.log('localStorage.clear() called');
} catch (e) {
console.error('Error clearing localStorage:', e);
}
@ -261,7 +256,6 @@ export class TokenManager {
allSessionStorageKeys.forEach(key => {
try {
sessionStorage.removeItem(key);
console.log(`Removed sessionStorage key: ${key}`);
} catch (e) {
console.warn(`Error removing sessionStorage key ${key}:`, e);
}
@ -269,7 +263,6 @@ export class TokenManager {
// Final clear as backup
sessionStorage.clear();
console.log('sessionStorage.clear() called');
} catch (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)
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');
// Note: httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this
// 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) {
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,
},
},
preview: {
port: 3000,
},
});