setup instruction added

This commit is contained in:
laxmanhalaki 2025-11-17 16:53:36 +05:30
parent 54462b1658
commit e193b60083
48 changed files with 539 additions and 1221 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
VITE_PUBLIC_VAPID_KEY={{TAKE_IT_FROM_BACKEND_ENV}}
VITE_BASE_URL={{BACKEND_BASE_URL}}
VITE_API_BASE_URL={{BACKEND_BASEURL+api/v1}}
VITE_OKTA_CLIENT_ID={{Client_id_given_by client for respective mode (UAT/DEVELOPMENT))
VITE_OKTA_DOMAIN={{OKTA_DOMAIN}}

View File

@ -1,3 +0,0 @@
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).

View File

@ -1,258 +0,0 @@
# Detailed Reports Page - Data Availability Analysis
## Overview
This document analyzes what data is currently available in the backend and what information is missing for implementing the DetailedReports page.
---
## 1. Request Lifecycle Report
### ✅ **Available Data:**
- **Request Basic Info:**
- `requestNumber` (RE-REQ-2024-XXX)
- `title`
- `priority` (STANDARD/EXPRESS)
- `status` (DRAFT, PENDING, IN_PROGRESS, APPROVED, REJECTED, CLOSED)
- `initiatorId` → Can get initiator name via User model
- `submissionDate`
- `closureDate`
- `createdAt`
- **Current Stage Info:**
- `currentLevel` (1-N)
- `totalLevels`
- Can get current approver from `approval_levels` table
- **TAT Information:**
- `totalTatHours` (cumulative TAT)
- Can calculate overall TAT from `submissionDate` to `closureDate` or `updatedAt`
- Can get level-wise TAT from `approval_levels.tat_hours`
- Can get TAT compliance from `tat_alerts` table
- **From Existing Services:**
- `getCriticalRequests()` - Returns requests with breach info
- `getUpcomingDeadlines()` - Returns active level info
- `getRecentActivity()` - Returns activity feed
### ❌ **Missing Data:**
1. **Current Stage Name/Description:**
- Need to join with `approval_levels` to get `level_name` for current level
- Currently only have `currentLevel` number
2. **Overall TAT Calculation:**
- Need API endpoint that calculates total time from submission to current/closure
- Currently have `totalTatHours` but need actual elapsed time
3. **TAT Compliance Status:**
- Need to determine if "On Time" or "Delayed" based on TAT vs actual time
- Can calculate from `tat_alerts.is_breached` but need endpoint
4. **Timeline/History:**
- Need endpoint to get all approval levels with their start/end times
- Need to show progression through levels
### 🔧 **What Needs to be Built:**
- **New API Endpoint:** `/dashboard/reports/lifecycle`
- Returns requests with:
- Full lifecycle timeline (all levels with dates)
- Overall TAT calculation
- TAT compliance status (On Time/Delayed)
- Current stage name
- All approvers in sequence
---
## 2. User Activity Log Report
### ✅ **Available Data:**
- **Activity Model Fields:**
- `activityId`
- `requestId`
- `userId` → Can get user name from User model
- `userName` (stored directly)
- `activityType` (created, assignment, approval, rejection, etc.)
- `activityDescription` (details of action)
- `ipAddress` (available in model, but may not be logged)
- `createdAt` (timestamp)
- `metadata` (JSONB - can store additional info)
- **From Existing Services:**
- `getRecentActivity()` - Already returns activity feed with pagination
- Returns: `activityId`, `requestId`, `requestNumber`, `requestTitle`, `type`, `action`, `details`, `userId`, `userName`, `timestamp`, `priority`
### ❌ **Missing Data:**
1. **IP Address:**
- Field exists in model but may not be populated
- Need to ensure IP is captured when logging activities
2. **User Agent/Device Info:**
- Field exists (`userAgent`) but may not be populated
- Need to capture browser/device info
3. **Login Activities:**
- Current activity model is request-focused
- Need separate user session/login tracking
- Can check `users.last_login` but need detailed login history
4. **Action Categorization:**
- Need to map `activityType` to display labels:
- "created" → "Created Request"
- "approval" → "Approved Request"
- "rejection" → "Rejected Request"
- "comment" → "Added Comment"
- etc.
5. **Request ID Display:**
- Need to show request number when available
- Currently `getRecentActivity()` returns `requestNumber`
### 🔧 **What Needs to be Built:**
- **Enhance Activity Logging:**
- Capture IP address in activity service
- Capture user agent in activity service
- Add login activity tracking (separate from request activities)
- **New/Enhanced API Endpoint:** `/dashboard/reports/activity-log`
- Filter by date range
- Filter by user
- Filter by action type
- Include IP address and user agent
- Better categorization of actions
---
## 3. Workflow Aging Report
### ✅ **Available Data:**
- **Request Basic Info:**
- `requestNumber`
- `title`
- `initiatorId` → Can get initiator name
- `priority`
- `status`
- `createdAt` (can calculate days open)
- `submissionDate`
- **Current Stage Info:**
- `currentLevel`
- `totalLevels`
- Can get current approver from `approval_levels`
- **From Existing Services:**
- `getUpcomingDeadlines()` - Returns active requests with TAT info
- Can filter by days open using `createdAt` or `submissionDate`
### ❌ **Missing Data:**
1. **Days Open Calculation:**
- Need to calculate from `submissionDate` (not `createdAt`)
- Need to exclude weekends/holidays for accurate business days
2. **Start Date:**
- Should use `submissionDate` (when request was submitted, not created)
- Currently have this field ✅
3. **Assigned To:**
- Need current approver from `approval_levels` where `level_number = current_level`
- Can get from `approval_levels.approver_name`
4. **Current Stage Name:**
- Need `approval_levels.level_name` for current level
- Currently only have level number
5. **Aging Threshold Filtering:**
- Need to filter requests where days open > threshold
- Need to calculate business days (excluding weekends/holidays)
### 🔧 **What Needs to be Built:**
- **New API Endpoint:** `/dashboard/reports/workflow-aging`
- Parameters:
- `threshold` (days)
- `dateRange` (optional)
- `page`, `limit` (pagination)
- Returns:
- Requests with days open > threshold
- Business days calculation
- Current stage name
- Current approver
- Days open (business days)
---
## Summary
### ✅ **Can Show Immediately:**
1. **Request Lifecycle Report (Partial):**
- Request ID, Title, Priority, Status
- Initiator name
- Submission date
- Current level number
- Basic TAT info
2. **User Activity Log (Partial):**
- Timestamp, User, Action, Details
- Request ID (when applicable)
- Using existing `getRecentActivity()` service
3. **Workflow Aging (Partial):**
- Request ID, Title, Initiator
- Days open (calendar days)
- Priority, Status
- Current approver (with join)
### ❌ **Missing/Incomplete:**
1. **Request Lifecycle:**
- Full timeline/history of all levels
- Current stage name (not just number)
- Overall TAT calculation
- TAT compliance status (On Time/Delayed)
2. **User Activity Log:**
- IP Address (field exists but may not be populated)
- User Agent (field exists but may not be populated)
- Login activities (separate tracking needed)
- Better action categorization
3. **Workflow Aging:**
- Business days calculation (excluding weekends/holidays)
- Current stage name
- Proper threshold filtering
### 🔧 **Required Backend Work:**
1. **New Endpoints:**
- `/dashboard/reports/lifecycle` - Full lifecycle with timeline
- `/dashboard/reports/activity-log` - Enhanced activity log with filters
- `/dashboard/reports/workflow-aging` - Aging report with business days
2. **Enhancements:**
- Capture IP address in activity logging
- Capture user agent in activity logging
- Add login activity tracking
- Add business days calculation utility
- Add level name to approval levels response
3. **Data Joins:**
- Join `approval_levels` to get current stage name
- Join `users` to get approver names
- Join `tat_alerts` to get breach/compliance info
---
## Recommendations
### Phase 1 (Quick Win - Use Existing Data):
- Implement basic reports using existing services
- Show available data (request info, basic activity, calendar days)
- Add placeholders for missing data
### Phase 2 (Backend Development):
- Build new report endpoints
- Enhance activity logging to capture IP/user agent
- Add business days calculation
- Add level name to responses
### Phase 3 (Full Implementation):
- Complete all three reports with full data
- Add filtering, sorting, export functionality
- Add date range filters
- Add user/role-based filtering

228
README.md
View File

@ -61,6 +61,15 @@ A modern, enterprise-grade approval and request management system built with Rea
## 🚀 Installation ## 🚀 Installation
### Quick Start Checklist
- [ ] Clone the repository
- [ ] Install Node.js (>= 18.0.0) and npm (>= 9.0.0)
- [ ] Install project dependencies
- [ ] Set up environment variables (`.env.local`)
- [ ] Ensure backend API is running (optional for initial setup)
- [ ] Start development server
### 1. Clone the repository ### 1. Clone the repository
\`\`\`bash \`\`\`bash
@ -76,36 +85,114 @@ npm install
### 3. Set up environment variables ### 3. Set up environment variables
#### Option A: Automated Setup (Recommended - Unix/Linux/Mac)
Run the setup script to automatically create environment files:
\`\`\`bash \`\`\`bash
cp .env.example .env chmod +x setup-env.sh
./setup-env.sh
\`\`\` \`\`\`
Edit `.env` with your configuration: This script will:
- Create `.env.example` with all required variables
- Create `.env.local` for local development
- Create `.env.production` with your production configuration (interactive)
#### Option B: Manual Setup (Windows or Custom Configuration)
**For Windows (PowerShell):**
1. Create `.env.local` file in the project root:
\`\`\`powershell
# Create .env.local file
New-Item -Path .env.local -ItemType File
\`\`\`
2. Add the following content to `.env.local`:
\`\`\`env \`\`\`env
VITE_API_BASE_URL=http://localhost:5000/api # Local Development Environment
VITE_APP_NAME=Royal Enfield Approval Portal VITE_API_BASE_URL=http://localhost:5000/api/v1
VITE_BASE_URL=http://localhost:5000
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=your-okta-domain.okta.com
VITE_OKTA_CLIENT_ID=your-okta-client-id
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=your-vapid-public-key
\`\`\` \`\`\`
### 4. Move files to src directory **For Production:**
Create `.env.production` with production values:
\`\`\`env
# Production Environment
VITE_API_BASE_URL=https://your-backend-url.com/api/v1
VITE_BASE_URL=https://your-backend-url.com
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=https://your-org.okta.com
VITE_OKTA_CLIENT_ID=your-production-client-id
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=your-production-vapid-key
\`\`\`
#### Environment Variables Reference
| 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_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 | - |
\*Required if using Okta authentication
### 4. Verify setup
Check that all required files exist:
\`\`\`bash \`\`\`bash
# Create src directory structure # Check environment file exists
mkdir -p src/components src/utils src/styles src/types ls -la .env.local # Unix/Linux/Mac
# or
# Move existing files (you'll need to do this manually or run the migration script) Test-Path .env.local # Windows PowerShell
# The structure should match the project structure below
\`\`\` \`\`\`
## 💻 Development ## 💻 Development
### Prerequisites
Before starting development, ensure:
1. **Backend API is running:**
- The backend should be running on `http://localhost:5000` (or your configured URL)
- Backend API should be accessible at `/api/v1` endpoint
- CORS should be configured to allow your frontend origin
2. **Environment variables are configured:**
- `.env.local` file exists and contains valid configuration
- All required variables are set (see [Environment Variables Reference](#environment-variables-reference))
3. **Node.js and npm versions:**
- Verify Node.js version: `node --version` (should be >= 18.0.0)
- Verify npm version: `npm --version` (should be >= 9.0.0)
### Start development server ### Start development server
\`\`\`bash \`\`\`bash
npm run dev npm run dev
\`\`\` \`\`\`
The application will open at `http://localhost:3000` The application will open at `http://localhost:5173` (Vite default port)
> **Note:** If port 5173 is in use, Vite will automatically use the next available port.
### Build for production ### Build for production
@ -183,6 +270,10 @@ import { Button } from '@/components/ui/button';
import { getDealerInfo } from '@/utils/dealerDatabase'; import { getDealerInfo } from '@/utils/dealerDatabase';
\`\`\` \`\`\`
Path aliases are configured in:
- `tsconfig.json` - TypeScript path mapping
- `vite.config.ts` - Vite resolver configuration
### Tailwind CSS Customization ### Tailwind CSS Customization
Custom Royal Enfield colors are defined in `tailwind.config.ts`: Custom Royal Enfield colors are defined in `tailwind.config.ts`:
@ -201,66 +292,95 @@ colors: {
All environment variables must be prefixed with `VITE_` to be accessible in the app: All environment variables must be prefixed with `VITE_` to be accessible in the app:
\`\`\`typescript \`\`\`typescript
// Access environment variables
const apiUrl = import.meta.env.VITE_API_BASE_URL; const apiUrl = import.meta.env.VITE_API_BASE_URL;
const baseUrl = import.meta.env.VITE_BASE_URL;
const oktaDomain = import.meta.env.VITE_OKTA_DOMAIN;
\`\`\` \`\`\`
## 🔧 Next Steps **Important Notes:**
- Environment variables are embedded at build time, not runtime
- Changes to `.env` files require restarting the dev server
- `.env.local` takes precedence over `.env` in development
- `.env.production` is used when building for production (`npm run build`)
### 1. File Migration ### Backend Integration
Move existing files to the `src` directory: To connect to the backend API:
1. **Update API base URL** in `.env.local`:
\`\`\`env
VITE_API_BASE_URL=http://localhost:5000/api/v1
\`\`\`
2. **Configure CORS** in your backend to allow your frontend origin
3. **Authentication:**
- Configure Okta credentials in environment variables
- Ensure backend validates JWT tokens from Okta
4. **API Services:**
- API services are located in `src/services/`
- All API calls use `axios` configured with base URL from environment
### Development vs Production
- **Development:** Uses `.env.local` (git-ignored)
- **Production:** Uses `.env.production` or environment variables set in deployment platform
- **Never commit:** `.env.local` or `.env.production` (use `.env.example` as template)
## 🔧 Troubleshooting
### Common Issues
#### Port Already in Use
If the default port (5173) is in use:
\`\`\`bash \`\`\`bash
# Move App.tsx # Option 1: Kill the process using the port
mv App.tsx src/ # Windows
netstat -ano | findstr :5173
taskkill /PID <PID> /F
# Move components # Unix/Linux/Mac
mv components src/ lsof -ti:5173 | xargs kill -9
# Move utils # Option 2: Use a different port
mv utils src/ npm run dev -- --port 3000
# Move styles
mv styles src/
\`\`\` \`\`\`
### 2. Create main.tsx entry point #### Environment Variables Not Loading
Create `src/main.tsx`: 1. Ensure variables are prefixed with `VITE_`
2. Restart the dev server after changing `.env` files
3. Check that `.env.local` exists in the project root
4. Verify no typos in variable names
\`\`\`typescript #### Backend Connection Issues
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/globals.css';
ReactDOM.createRoot(document.getElementById('root')!).render( 1. Verify backend is running on the configured port
<React.StrictMode> 2. Check `VITE_API_BASE_URL` in `.env.local` matches backend URL
<App /> 3. Ensure CORS is configured in backend to allow frontend origin
</React.StrictMode> 4. Check browser console for detailed error messages
);
#### Build Errors
\`\`\`bash
# Clear cache and rebuild
rm -rf node_modules/.vite
npm run build
# Check for TypeScript errors
npm run type-check
\`\`\` \`\`\`
### 3. Update imports ### Getting Help
Update all import paths to use the `@/` alias: - Check browser console for errors
- Verify all environment variables are set correctly
\`\`\`typescript - Ensure Node.js and npm versions meet requirements
// Before - Review backend logs for API-related issues
import { Button } from './components/ui/button';
// After
import { Button } from '@/components/ui/button';
\`\`\`
### 4. Backend Integration
When ready to connect to a real API:
1. Create `src/services/api.ts` for API calls
2. Replace mock databases with API calls
3. Add authentication layer
4. Implement error handling
## 🧪 Testing (Future Enhancement) ## 🧪 Testing (Future Enhancement)

View File

@ -1,193 +0,0 @@
# Frontend Role Migration - isAdmin → role
## 🎯 Overview
Migrated frontend from `isAdmin: boolean` to `role: 'USER' | 'MANAGEMENT' | 'ADMIN'` to match the new backend RBAC system.
---
## ✅ Files Updated
### 1. **Type Definitions**
#### `src/contexts/AuthContext.tsx`
- ✅ Updated `User` interface: `isAdmin?: boolean``role?: 'USER' | 'MANAGEMENT' | 'ADMIN'`
- ✅ Added helper functions:
- `isAdmin(user)` - Checks if user is ADMIN
- `isManagement(user)` - Checks if user is MANAGEMENT
- `hasManagementAccess(user)` - Checks if user is MANAGEMENT or ADMIN
- `hasAdminAccess(user)` - Checks if user is ADMIN (same as isAdmin)
#### `src/services/authApi.ts`
- ✅ Updated `TokenExchangeResponse` interface: `isAdmin: boolean``role: 'USER' | 'MANAGEMENT' | 'ADMIN'`
---
### 2. **Components Updated**
#### `src/pages/Dashboard/Dashboard.tsx`
**Changes:**
- ✅ Imported `isAdmin as checkIsAdmin` from AuthContext
- ✅ Updated role check: `(user as any)?.isAdmin || false``checkIsAdmin(user)`
- ✅ All conditional rendering now uses the helper function
**Admin Features (shown only for ADMIN role):**
- Organization-wide analytics
- Admin View badge
- Export button
- Department-wise workflow summary
- Priority distribution report
- TAT breach report
- AI remark utilization report
- Approver performance report
---
#### `src/pages/Settings/Settings.tsx`
**Changes:**
- ✅ Imported `isAdmin as checkIsAdmin` from AuthContext
- ✅ Updated role check: `(user as any)?.isAdmin``checkIsAdmin(user)`
**Admin Features:**
- Configuration Manager tab
- Holiday Manager tab
- System Settings tab
---
#### `src/pages/Profile/Profile.tsx`
**Changes:**
- ✅ Imported `isAdmin` and `isManagement` helpers from AuthContext
- ✅ Added `Users` icon import for Management badge
- ✅ Updated all `user?.isAdmin` checks to use `isAdmin(user)`
- ✅ Added Management badge display for MANAGEMENT role
- ✅ Updated role display to show:
- **Administrator** badge (yellow) for ADMIN
- **Management** badge (blue) for MANAGEMENT
- **User** badge (gray) for USER
**New Visual Indicators:**
- 🟡 Yellow shield icon for ADMIN users
- 🔵 Blue users icon for MANAGEMENT users
- Role badge on profile card
- Role badge in header section
---
#### `src/pages/Auth/AuthenticatedApp.tsx`
**Changes:**
- ✅ Updated console log: `'Is Admin:', user.isAdmin``'Role:', user.role`
---
## 🎨 **Visual Changes**
### Profile Page Badges
**Before:**
```
🟡 Administrator (only for admins)
```
**After:**
```
🟡 Administrator (for ADMIN)
🔵 Management (for MANAGEMENT)
```
### Role Display
**Before:**
- Administrator / User
**After:**
- Administrator (yellow badge, green checkmark)
- Management (blue badge, green checkmark)
- User (gray badge, no checkmark)
---
## 🔧 **Helper Functions Usage**
### In Components:
```typescript
import { useAuth, isAdmin, isManagement, hasManagementAccess } from '@/contexts/AuthContext';
const { user } = useAuth();
// Check if user is admin
if (isAdmin(user)) {
// Show admin-only features
}
// Check if user is management
if (isManagement(user)) {
// Show management-only features
}
// Check if user has management access (MANAGEMENT or ADMIN)
if (hasManagementAccess(user)) {
// Show features for both management and admin
}
```
---
## 🚀 **Migration Benefits**
1. **Type Safety** - Role is now a union type, catching errors at compile time
2. **Flexibility** - Easy to add more roles (e.g., AUDITOR, VIEWER)
3. **Granular Access** - Can differentiate between MANAGEMENT and ADMIN
4. **Consistency** - Frontend now matches backend RBAC system
5. **Helper Functions** - Cleaner code with reusable role checks
---
## 📊 **Access Levels**
| Feature | USER | MANAGEMENT | ADMIN |
|---------|------|------------|-------|
| View own requests | ✅ | ✅ | ✅ |
| View own dashboard | ✅ | ✅ | ✅ |
| View all requests | ❌ | ✅ | ✅ |
| View organization-wide analytics | ❌ | ✅ | ✅ |
| Export data | ❌ | ❌ | ✅ |
| Manage system configuration | ❌ | ❌ | ✅ |
| Manage holidays | ❌ | ❌ | ✅ |
| View TAT breach reports | ❌ | ❌ | ✅ |
| View approver performance | ❌ | ❌ | ✅ |
---
## ✅ **Testing Checklist**
- [ ] Login as USER - verify limited access
- [ ] Login as MANAGEMENT - verify read access to all data
- [ ] Login as ADMIN - verify full access
- [ ] Profile page shows correct role badge
- [ ] Dashboard shows appropriate views per role
- [ ] Settings page shows tabs only for ADMIN
- [ ] No console errors related to role checks
---
## 🔄 **Backward Compatibility**
**None** - This is a breaking change. All users must be assigned a role in the database:
```sql
-- Default all users to USER role
UPDATE users SET role = 'USER' WHERE role IS NULL;
-- Assign specific roles
UPDATE users SET role = 'ADMIN' WHERE email = 'admin@royalenfield.com';
UPDATE users SET role = 'MANAGEMENT' WHERE email = 'manager@royalenfield.com';
```
---
## 🎉 **Deployment Ready**
All changes are complete and linter-clean. Frontend now fully supports the new RBAC system!

View File

@ -1,339 +0,0 @@
# User Role Management Feature
## 🎯 Overview
Added a comprehensive User Role Management system for administrators to assign roles to users directly from the Settings page.
---
## ✅ What Was Built
### Frontend Components
#### 1. **UserRoleManager Component**
Location: `src/components/admin/UserRoleManager/UserRoleManager.tsx`
**Features:**
- **Search Users from Okta** - Real-time search with debouncing
- **Role Assignment** - Assign USER, MANAGEMENT, or ADMIN roles
- **Statistics Dashboard** - Shows count of users in each role
- **Elevated Users List** - Displays all ADMIN and MANAGEMENT users
- **Auto-create Users** - If user doesn't exist in database, fetches from Okta and creates them
- **Self-demotion Prevention** - Admin cannot demote themselves
**UI Components:**
- Statistics cards showing admin/management/user counts
- Search input with dropdown results
- Selected user card display
- Role selector dropdown
- Assign button with loading state
- Success/error message display
- Elevated users list with role badges
---
### Backend APIs
#### 2. **New Route: Assign Role by Email**
`POST /api/v1/admin/users/assign-role`
**Purpose:** Assign role to user by email (creates user from Okta if doesn't exist)
**Request:**
```json
{
"email": "user@royalenfield.com",
"role": "MANAGEMENT" // or "USER" or "ADMIN"
}
```
**Response:**
```json
{
"success": true,
"message": "Successfully assigned MANAGEMENT role to John Doe",
"data": {
"userId": "abc-123",
"email": "user@royalenfield.com",
"displayName": "John Doe",
"role": "MANAGEMENT"
}
}
```
**Flow:**
1. Check if user exists in database by email
2. If not exists → Search Okta API
3. If found in Okta → Create user in database with assigned role
4. If exists → Update user's role
5. Prevent self-demotion (admin demoting themselves)
---
#### 3. **Existing Routes (Already Created)**
**Get Users by Role**
```
GET /api/v1/admin/users/by-role?role=ADMIN
GET /api/v1/admin/users/by-role?role=MANAGEMENT
```
**Get Role Statistics**
```
GET /api/v1/admin/users/role-statistics
```
Response:
```json
{
"success": true,
"data": {
"statistics": [
{ "role": "ADMIN", "count": 3 },
{ "role": "MANAGEMENT", "count": 12 },
{ "role": "USER", "count": 145 }
],
"total": 160
}
}
```
**Update User Role by ID**
```
PUT /api/v1/admin/users/:userId/role
Body: { "role": "MANAGEMENT" }
```
---
### Settings Page Updates
#### 4. **New Tab: "User Roles"**
Location: `src/pages/Settings/Settings.tsx`
**Changes:**
- Added 4th tab to admin settings
- Tab layout now responsive: 2 columns on mobile, 4 on desktop
- Tab order: User Settings → **User Roles** → Configuration → Holidays
- Only visible to ADMIN role users
**Tab Structure:**
```
┌─────────────┬────────────┬──────────────┬──────────┐
│ User │ User Roles │ Config │ Holidays │
│ Settings │ (NEW! ✨) │ │ │
└─────────────┴────────────┴──────────────┴──────────┘
```
---
### API Service Updates
#### 5. **User API Service**
Location: `src/services/userApi.ts`
**New Functions:**
```typescript
userApi.assignRole(email, role) // Assign role by email
userApi.updateUserRole(userId, role) // Update role by userId
userApi.getUsersByRole(role) // Get users filtered by role
userApi.getRoleStatistics() // Get role counts
```
---
## 🎨 UI/UX Features
### Statistics Cards
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Administrators │ │ Management │ │ Regular Users │
│ 3 │ │ 12 │ │ 145 │
│ 👑 ADMIN │ │ 👥 MANAGEMENT │ │ 👤 USER │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```
### Role Assignment Section
1. **Search Input** - Type name or email
2. **Results Dropdown** - Shows matching Okta users
3. **Selected User Card** - Displays chosen user details
4. **Role Selector** - Dropdown with 3 role options
5. **Assign Button** - Confirms role assignment
### Elevated Users List
- Shows all ADMIN and MANAGEMENT users
- Regular USER role users are not shown (too many)
- Each user card shows:
- Role icon and badge
- Display name
- Email
- Department and designation
---
## 🔐 Access Control
### ADMIN Only
- View User Roles tab
- Search and assign roles
- View all elevated users
- Create users from Okta
- Demote users (except themselves)
### MANAGEMENT & USER
- Cannot access User Roles tab
- See info message about admin features
---
## 🔄 User Creation Flow
### Scenario 1: User Exists in Database
```
1. Admin searches "john@royalenfield.com"
2. Finds user in search results
3. Selects user
4. Assigns MANAGEMENT role
5. ✅ User role updated
```
### Scenario 2: User Doesn't Exist in Database
```
1. Admin searches "new.user@royalenfield.com"
2. Finds user in Okta search results
3. Selects user
4. Assigns MANAGEMENT role
5. Backend fetches full details from Okta
6. Creates user in database with MANAGEMENT role
7. ✅ User created and role assigned
```
### Scenario 3: User Not in Okta
```
1. Admin searches "fake@email.com"
2. No results found
3. If admin types email manually and tries to assign
4. ❌ Error: "User not found in Okta. Please ensure the email is correct."
```
---
## 🎯 Role Badge Colors
| Role | Badge Color | Icon | Access Level |
|------|-------------|------|--------------|
| ADMIN | 🟡 Yellow | 👑 Crown | Full system access |
| MANAGEMENT | 🔵 Blue | 👥 Users | Read all data, enhanced dashboards |
| USER | ⚪ Gray | 👤 User | Own requests and assigned workflows |
---
## 📊 Test Scenarios
### Test 1: Assign MANAGEMENT Role to Existing User
```
1. Login as ADMIN
2. Go to Settings → User Roles tab
3. Search for existing user
4. Select MANAGEMENT role
5. Click Assign Role
6. Verify success message
7. Check user appears in Elevated Users list
```
### Test 2: Create New User from Okta
```
1. Search for user not in database (but in Okta)
2. Select ADMIN role
3. Click Assign Role
4. Verify user is created AND role assigned
5. Check statistics update (+1 ADMIN)
```
### Test 3: Self-Demotion Prevention
```
1. Login as ADMIN
2. Search for your own email
3. Try to assign USER or MANAGEMENT role
4. Verify error: "You cannot demote yourself from ADMIN role"
```
### Test 4: Role Statistics
```
1. Check statistics cards show correct counts
2. Assign roles to users
3. Verify statistics update in real-time
```
---
## 🔧 Backend Implementation Details
### Controller: `admin.controller.ts`
**New Function: `assignRoleByEmail`**
```typescript
1. Validate email and role
2. Check if user exists in database
3. If NOT exists:
a. Import UserService
b. Search Okta by email
c. If not found in Okta → return 404
d. If found → Create user with assigned role
4. If EXISTS:
a. Check for self-demotion
b. Update user's role
5. Return success response
```
---
## 📁 Files Modified
### Frontend (3 new, 2 modified)
```
✨ src/components/admin/UserRoleManager/UserRoleManager.tsx (NEW)
✨ src/components/admin/UserRoleManager/index.ts (NEW)
✨ Re_Figma_Code/USER_ROLE_MANAGEMENT.md (NEW - this file)
✏️ src/services/userApi.ts (MODIFIED - added 4 functions)
✏️ src/pages/Settings/Settings.tsx (MODIFIED - added User Roles tab)
```
### Backend (2 modified)
```
✏️ src/controllers/admin.controller.ts (MODIFIED - added assignRoleByEmail)
✏️ src/routes/admin.routes.ts (MODIFIED - added POST /users/assign-role)
```
---
## 🎉 Complete Feature Set
✅ Search users from Okta
✅ Create users from Okta if they don't exist
✅ Assign any of 3 roles (USER, MANAGEMENT, ADMIN)
✅ View role statistics
✅ View all elevated users (ADMIN + MANAGEMENT)
✅ Regular users hidden (don't clutter the list)
✅ Self-demotion prevention
✅ Real-time search with debouncing
✅ Beautiful UI with gradient cards
✅ Role badges with icons
✅ Success/error messaging
✅ Loading states
✅ Test IDs for testing
✅ Mobile responsive
✅ Admin-only access
---
## 🚀 Ready to Use!
The feature is fully functional and ready for testing. Admins can now easily manage user roles directly from the Settings page without needing SQL or manual database access!
**To test:**
1. Log in as ADMIN user
2. Navigate to Settings
3. Click "User Roles" tab
4. Start assigning roles! 🎯

28
package-lock.json generated
View File

@ -52,7 +52,9 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.0",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",
"recharts": "^2.13.3", "recharts": "^2.13.3",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
@ -6117,6 +6119,22 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.53.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
"integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -6203,6 +6221,16 @@
} }
} }
}, },
"node_modules/react-resizable-panels": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.0.tgz",
"integrity": "sha512-k2gGjGyCNF9xq8gVkkHBK1mlWv6xetPtvRdEtD914gTdhJcy02TLF0xMPuVLlGRuLoWGv7Gd/O1rea2KIQb3Qw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.9.4", "version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",

View File

@ -57,7 +57,9 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-resizable-panels": "^2.1.0",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",
"recharts": "^2.13.3", "recharts": "^2.13.3",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",

View File

@ -1,97 +0,0 @@
@echo off
REM Environment Setup Script for Royal Enfield Workflow Frontend (Windows)
echo ==================================================
echo Royal Enfield - Frontend Environment Setup
echo ==================================================
echo.
echo This script will create environment configuration files for your frontend.
echo.
REM Check if files already exist
if exist ".env.local" (
echo WARNING: .env.local already exists
set FILE_EXISTS=1
)
if exist ".env.production" (
echo WARNING: .env.production already exists
set FILE_EXISTS=1
)
if defined FILE_EXISTS (
echo.
set /p OVERWRITE="Do you want to OVERWRITE existing files? (y/n): "
if /i not "%OVERWRITE%"=="y" (
echo Aborted. No files were modified.
exit /b 0
)
)
REM Create .env.example
echo # API Configuration> .env.example
echo # Backend API base URL (with /api/v1)>> .env.example
echo VITE_API_BASE_URL=http://localhost:5000/api/v1>> .env.example
echo.>> .env.example
echo # Base URL for direct file access (without /api/v1)>> .env.example
echo VITE_BASE_URL=http://localhost:5000>> .env.example
echo Created .env.example
REM Create .env.local
echo # Local Development Environment> .env.local
echo VITE_API_BASE_URL=http://localhost:5000/api/v1>> .env.local
echo VITE_BASE_URL=http://localhost:5000>> .env.local
echo Created .env.local (for local development)
REM Create .env.production
echo.
echo ==================================================
echo Production Environment Configuration
echo ==================================================
echo.
set /p BACKEND_URL="Enter your PRODUCTION backend URL (e.g., https://api.yourcompany.com): "
if "%BACKEND_URL%"=="" (
echo WARNING: No backend URL provided. Creating template file...
echo # Production Environment> .env.production
echo # IMPORTANT: Update these URLs with your actual deployed backend URL>> .env.production
echo VITE_API_BASE_URL=https://your-backend-url.com/api/v1>> .env.production
echo VITE_BASE_URL=https://your-backend-url.com>> .env.production
) else (
REM Remove trailing slash if present
if "%BACKEND_URL:~-1%"=="/" set BACKEND_URL=%BACKEND_URL:~0,-1%
echo # Production Environment> .env.production
echo VITE_API_BASE_URL=%BACKEND_URL%/api/v1>> .env.production
echo VITE_BASE_URL=%BACKEND_URL%>> .env.production
echo Created .env.production with backend URL: %BACKEND_URL%
)
echo.
echo ==================================================
echo Setup Complete!
echo ==================================================
echo.
echo Next Steps:
echo.
echo 1. For LOCAL development:
echo npm run dev
echo (will use .env.local automatically)
echo.
echo 2. For PRODUCTION deployment:
echo - If deploying to Vercel/Netlify/etc:
echo Set environment variables in your platform dashboard
echo - If using Docker/VM:
echo Ensure .env.production has correct URLs
echo.
echo 3. Update Okta Configuration:
echo - Add production callback URL to Okta app settings
echo - Sign-in redirect URI: https://your-frontend.com/login/callback
echo.
echo 4. Update Backend CORS:
echo - Add production frontend URL to CORS allowed origins
echo.
echo For detailed instructions, see: DEPLOYMENT_CONFIGURATION.md
echo.
pause

View File

@ -15,6 +15,13 @@ VITE_API_BASE_URL=http://localhost:5000/api/v1
# Base URL for direct file access (without /api/v1) # Base URL for direct file access (without /api/v1)
VITE_BASE_URL=http://localhost:5000 VITE_BASE_URL=http://localhost:5000
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=
VITE_OKTA_CLIENT_ID=
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=
EOF EOF
echo "✅ Created .env.example" echo "✅ Created .env.example"
} }
@ -25,6 +32,13 @@ create_env_local() {
# Local Development Environment # Local Development Environment
VITE_API_BASE_URL=http://localhost:5000/api/v1 VITE_API_BASE_URL=http://localhost:5000/api/v1
VITE_BASE_URL=http://localhost:5000 VITE_BASE_URL=http://localhost:5000
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=
VITE_OKTA_CLIENT_ID=
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=
EOF EOF
echo "✅ Created .env.local (for local development)" echo "✅ Created .env.local (for local development)"
} }
@ -37,25 +51,55 @@ create_env_production() {
echo "==================================================" echo "=================================================="
echo "" echo ""
read -p "Enter your PRODUCTION backend URL (e.g., https://api.yourcompany.com): " BACKEND_URL read -p "Enter your PRODUCTION backend URL (e.g., https://api.yourcompany.com): " BACKEND_URL
read -p "Enter your Okta Domain (e.g., https://your-org.okta.com): " OKTA_DOMAIN
read -p "Enter your Okta Client ID: " OKTA_CLIENT_ID
read -p "Enter your VAPID Public Key (for push notifications, optional): " VAPID_KEY
# Remove trailing slash if present
if [ ! -z "$BACKEND_URL" ]; then
BACKEND_URL=${BACKEND_URL%/}
fi
if [ ! -z "$OKTA_DOMAIN" ]; then
OKTA_DOMAIN=${OKTA_DOMAIN%/}
fi
if [ -z "$BACKEND_URL" ]; then if [ -z "$BACKEND_URL" ]; then
echo "⚠️ No backend URL provided. Creating template file..." echo "⚠️ No backend URL provided. Creating template file..."
cat > .env.production << 'EOF' cat > .env.production << 'EOF'
# Production Environment # Production Environment
# IMPORTANT: Update these URLs with your actual deployed backend URL # IMPORTANT: Update these values with your actual production configuration
# API Configuration
VITE_API_BASE_URL=https://your-backend-url.com/api/v1 VITE_API_BASE_URL=https://your-backend-url.com/api/v1
VITE_BASE_URL=https://your-backend-url.com VITE_BASE_URL=https://your-backend-url.com
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=
VITE_OKTA_CLIENT_ID=
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=
EOF EOF
else else
# Remove trailing slash if present
BACKEND_URL=${BACKEND_URL%/}
cat > .env.production << EOF cat > .env.production << EOF
# Production Environment # Production Environment
# API Configuration
VITE_API_BASE_URL=${BACKEND_URL}/api/v1 VITE_API_BASE_URL=${BACKEND_URL}/api/v1
VITE_BASE_URL=${BACKEND_URL} VITE_BASE_URL=${BACKEND_URL}
# Okta Authentication Configuration
VITE_OKTA_DOMAIN=${OKTA_DOMAIN}
VITE_OKTA_CLIENT_ID=${OKTA_CLIENT_ID}
# Push Notifications (Web Push / VAPID)
VITE_PUBLIC_VAPID_KEY=${VAPID_KEY}
EOF EOF
echo "✅ Created .env.production with backend URL: ${BACKEND_URL}" echo "✅ Created .env.production with:"
echo " - Backend URL: ${BACKEND_URL}"
[ ! -z "$OKTA_DOMAIN" ] && echo " - Okta Domain: ${OKTA_DOMAIN}"
[ ! -z "$OKTA_CLIENT_ID" ] && echo " - Okta Client ID: ${OKTA_CLIENT_ID}"
[ ! -z "$VAPID_KEY" ] && echo " - VAPID Key: Configured"
fi fi
} }
@ -99,11 +143,7 @@ echo " Set environment variables in your platform dashboard"
echo " - If using Docker/VM:" echo " - If using Docker/VM:"
echo " Ensure .env.production has correct URLs" echo " Ensure .env.production has correct URLs"
echo "" echo ""
echo "3. Update Okta Configuration:" echo "3. Update Backend CORS:"
echo " - Add production callback URL to Okta app settings"
echo " - Sign-in redirect URI: https://your-frontend.com/login/callback"
echo ""
echo "4. Update Backend CORS:"
echo " - Add production frontend URL to CORS allowed origins" echo " - Add production frontend URL to CORS allowed origins"
echo "" echo ""
echo "📖 For detailed instructions, see: DEPLOYMENT_CONFIGURATION.md" echo "📖 For detailed instructions, see: DEPLOYMENT_CONFIGURATION.md"

View File

@ -239,57 +239,6 @@ function AppRoutes({ onLogout }: AppProps) {
setApprovalAction(null); setApprovalAction(null);
}; };
const handleOpenModal = (modal: string) => {
switch (modal) {
case 'work-note':
navigate(`/work-notes/${selectedRequestId}`);
break;
case 'internal-chat':
toast.success('Internal Chat Opened', {
description: 'Internal chat opened for request stakeholders.',
});
break;
case 'approval-list':
toast.info('Approval List', {
description: 'Detailed approval workflow would be displayed.',
});
break;
case 'approve':
setApprovalAction('approve');
break;
case 'reject':
setApprovalAction('reject');
break;
case 'escalate':
toast.warning('Request Escalated', {
description: 'The request has been escalated to higher authority.',
});
break;
case 'reminder':
toast.info('Reminder Sent', {
description: 'Reminder notification sent to current approver.',
});
break;
case 'add-approver':
toast.info('Add Approver', {
description: 'Add approver functionality would be implemented here.',
});
break;
case 'add-spectator':
toast.info('Add Spectator', {
description: 'Add spectator functionality would be implemented here.',
});
break;
case 'modify-sla':
toast.info('Modify SLA', {
description: 'SLA modification functionality would be implemented here.',
});
break;
default:
break;
}
};
const handleClaimManagementSubmit = (claimData: any) => { const handleClaimManagementSubmit = (claimData: any) => {
// Generate unique ID for the new claim request // Generate unique ID for the new claim request
const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`; const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`;

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import { useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react'; import { useAuth0 } from '@auth0/auth0-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Save, Loader2, Sparkles, Eye, EyeOff } from 'lucide-react'; import { Save, Loader2, Sparkles } from 'lucide-react';
import { AIProviderSettings } from './AIProviderSettings'; import { AIProviderSettings } from './AIProviderSettings';
import { AIFeatures } from './AIFeatures'; import { AIFeatures } from './AIFeatures';
import { AIParameters } from './AIParameters'; import { AIParameters } from './AIParameters';

View File

@ -49,28 +49,6 @@ export function AIProviderSettings({
onToggleApiKeyVisibility, onToggleApiKeyVisibility,
maskApiKey maskApiKey
}: AIProviderSettingsProps) { }: AIProviderSettingsProps) {
const getCurrentApiKey = (provider: 'claude' | 'openai' | 'gemini'): string => {
switch (provider) {
case 'claude':
return claudeApiKey;
case 'openai':
return openaiApiKey;
case 'gemini':
return geminiApiKey;
}
};
const getApiKeyChangeHandler = (provider: 'claude' | 'openai' | 'gemini') => {
switch (provider) {
case 'claude':
return onClaudeApiKeyChange;
case 'openai':
return onOpenaiApiKeyChange;
case 'gemini':
return onGeminiApiKeyChange;
}
};
return ( return (
<Card className="border-0 shadow-sm"> <Card className="border-0 shadow-sm">
<CardHeader className="pb-3"> <CardHeader className="pb-3">

View File

@ -153,7 +153,6 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
const renderConfigInput = (config: AdminConfiguration) => { const renderConfigInput = (config: AdminConfiguration) => {
const currentValue = getCurrentValue(config); const currentValue = getCurrentValue(config);
const isChanged = hasChanges(config);
const isSaving = saving === config.configKey; const isSaving = saving === config.configKey;
if (!config.isEditable) { if (!config.isEditable) {
@ -203,7 +202,11 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
min={min} min={min}
max={max} max={max}
step={1} step={1}
onValueChange={([value]) => handleValueChange(config.configKey, value.toString())} onValueChange={([value]) => {
if (value !== undefined) {
handleValueChange(config.configKey, value.toString());
}
}}
disabled={isSaving} disabled={isSaving}
className="w-full" className="w-full"
/> />
@ -276,13 +279,16 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
if (!acc[config.configCategory]) { if (!acc[config.configCategory]) {
acc[config.configCategory] = []; acc[config.configCategory] = [];
} }
acc[config.configCategory].push(config); acc[config.configCategory]!.push(config);
return acc; return acc;
}, {} as Record<string, AdminConfiguration[]>); }, {} as Record<string, AdminConfiguration[]>);
// Sort configs within each category by sortOrder // Sort configs within each category by sortOrder
Object.keys(groupedConfigs).forEach(category => { Object.keys(groupedConfigs).forEach(category => {
groupedConfigs[category].sort((a, b) => a.sortOrder - b.sortOrder); const categoryConfigs = groupedConfigs[category];
if (categoryConfigs) {
categoryConfigs.sort((a, b) => a.sortOrder - b.sortOrder);
}
}); });
if (loading) { if (loading) {
@ -370,13 +376,13 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
{category.replace(/_/g, ' ')} {category.replace(/_/g, ' ')}
</CardTitle> </CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
{groupedConfigs[category].length} setting{groupedConfigs[category].length !== 1 ? 's' : ''} available {groupedConfigs[category]?.length || 0} setting{(groupedConfigs[category]?.length || 0) !== 1 ? 's' : ''} available
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{groupedConfigs[category].map(config => ( {groupedConfigs[category]?.map(config => (
<div key={config.configKey} className="space-y-3 pb-6 border-b border-slate-100 last:border-b-0 last:pb-0 hover:bg-slate-50/50 -mx-6 px-6 py-4 rounded-md transition-colors"> <div key={config.configKey} className="space-y-3 pb-6 border-b border-slate-100 last:border-b-0 last:pb-0 hover:bg-slate-50/50 -mx-6 px-6 py-4 rounded-md transition-colors">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">

View File

@ -92,7 +92,7 @@ export function DashboardConfig() {
<RoleDashboardSection <RoleDashboardSection
key={role} key={role}
role={role} role={role}
kpis={config[role]} kpis={config[role] || {}}
onKPIToggle={(kpi, checked) => handleKPIToggle(role, kpi, checked)} onKPIToggle={(kpi, checked) => handleKPIToggle(role, kpi, checked)}
/> />
))} ))}

View File

@ -27,7 +27,6 @@ import {
Loader2, Loader2,
AlertCircle, AlertCircle,
CheckCircle, CheckCircle,
Upload
} from 'lucide-react'; } from 'lucide-react';
import { getAllHolidays, createHoliday, updateHoliday, deleteHoliday, Holiday } from '@/services/adminApi'; import { getAllHolidays, createHoliday, updateHoliday, deleteHoliday, Holiday } from '@/services/adminApi';
import { formatDateShort } from '@/utils/dateFormatter'; import { formatDateShort } from '@/utils/dateFormatter';
@ -267,7 +266,7 @@ export function HolidayManager() {
<div> <div>
<CardTitle className="text-base sm:text-lg font-semibold text-slate-900">{month} {selectedYear}</CardTitle> <CardTitle className="text-base sm:text-lg font-semibold text-slate-900">{month} {selectedYear}</CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
{holidaysByMonth[month].length} holiday{holidaysByMonth[month].length !== 1 ? 's' : ''} {holidaysByMonth[month]?.length || 0} holiday{(holidaysByMonth[month]?.length || 0) !== 1 ? 's' : ''}
</CardDescription> </CardDescription>
</div> </div>
<div className="p-2 bg-blue-50 rounded-md"> <div className="p-2 bg-blue-50 rounded-md">
@ -276,7 +275,7 @@ export function HolidayManager() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 pt-4"> <CardContent className="space-y-3 pt-4">
{holidaysByMonth[month].map(holiday => ( {holidaysByMonth[month]?.map(holiday => (
<div <div
key={holiday.holidayId} key={holiday.holidayId}
className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 p-3 sm:p-4 bg-slate-50 border border-slate-200 rounded-md hover:bg-slate-100 hover:border-slate-300 transition-all shadow-sm" className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 p-3 sm:p-4 bg-slate-50 border border-slate-200 rounded-md hover:bg-slate-100 hover:border-slate-300 transition-all shadow-sm"

View File

@ -42,7 +42,11 @@ export function EscalationSettings({
min={1} min={1}
max={100} max={100}
step={1} step={1}
onValueChange={([value]) => onReminderThreshold1Change(value)} onValueChange={([value]) => {
if (value !== undefined) {
onReminderThreshold1Change(value);
}
}}
className="w-full" className="w-full"
/> />
<p className="text-xs text-muted-foreground flex items-center gap-1"> <p className="text-xs text-muted-foreground flex items-center gap-1">
@ -64,7 +68,11 @@ export function EscalationSettings({
min={1} min={1}
max={100} max={100}
step={1} step={1}
onValueChange={([value]) => onReminderThreshold2Change(value)} onValueChange={([value]) => {
if (value !== undefined) {
onReminderThreshold2Change(value);
}
}}
className="w-full" className="w-full"
/> />
<p className="text-xs text-muted-foreground flex items-center gap-1"> <p className="text-xs text-muted-foreground flex items-center gap-1">

View File

@ -2,7 +2,6 @@ import { useState, useCallback, useEffect, useRef } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { import {
Select, Select,
SelectContent, SelectContent,
@ -15,15 +14,11 @@ import {
Search, Search,
Users, Users,
Shield, Shield,
UserCog,
Loader2, Loader2,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
Crown, Crown,
User as UserIcon, User as UserIcon,
Edit,
Trash2,
Power
} from 'lucide-react'; } from 'lucide-react';
import { userApi } from '@/services/userApi'; import { userApi } from '@/services/userApi';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -355,27 +350,6 @@ export function UserManagement() {
}; };
}, [searchResults]); }, [searchResults]);
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'ADMIN':
return 'bg-yellow-400 text-slate-900';
case 'MANAGEMENT':
return 'bg-blue-400 text-slate-900';
default:
return 'bg-gray-400 text-white';
}
};
const getRoleIcon = (role: string) => {
switch (role) {
case 'ADMIN':
return <Crown className="w-5 h-5" />;
case 'MANAGEMENT':
return <Users className="w-5 h-5" />;
default:
return <UserIcon className="w-5 h-5" />;
}
};
// Calculate stats for UserStatsCards // Calculate stats for UserStatsCards
const stats = { const stats = {

View File

@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { CheckCircle, AlertCircle } from 'lucide-react'; import { CheckCircle } from 'lucide-react';
type ApprovalModalProps = { type ApprovalModalProps = {
open: boolean; open: boolean;

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';

View File

@ -1,4 +1,4 @@
import React, { Component, ErrorInfo, ReactNode } from 'react'; import { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { AlertTriangle, RefreshCw, ArrowLeft } from 'lucide-react'; import { AlertTriangle, RefreshCw, ArrowLeft } from 'lucide-react';
@ -32,7 +32,7 @@ export class ErrorBoundary extends Component<Props, State> {
}; };
} }
componentDidCatch(error: Error, errorInfo: ErrorInfo) { override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error Boundary caught an error:', error, errorInfo); console.error('Error Boundary caught an error:', error, errorInfo);
this.setState({ this.setState({
@ -54,7 +54,7 @@ export class ErrorBoundary extends Component<Props, State> {
}); });
}; };
render() { override render() {
if (this.state.hasError) { if (this.state.hasError) {
// Custom fallback if provided // Custom fallback if provided
if (this.props.fallback) { if (this.props.fallback) {

View File

@ -1,4 +1,3 @@
import React from 'react';
interface LoaderProps { interface LoaderProps {
message?: string; message?: string;

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Bell, Settings, User, Plus, Search, Home, FileText, CheckCircle, LogOut, PanelLeft, PanelLeftClose, Activity, Shield } from 'lucide-react'; import { Bell, Settings, User, Plus, Search, Home, FileText, CheckCircle, LogOut, PanelLeft, PanelLeftClose, Shield } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -143,8 +143,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
fetchNotifications(); fetchNotifications();
// Setup socket for real-time notifications // Setup socket for real-time notifications
const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; const socket = getSocket(); // Uses getSocketBaseUrl() helper internally
const socket = getSocket(baseUrl);
if (socket) { if (socket) {
// Join user's personal notification room // Join user's personal notification room

View File

@ -10,11 +10,10 @@ interface AddUserModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
type: 'approver' | 'spectator'; type: 'approver' | 'spectator';
requestId: string;
requestTitle: string; requestTitle: string;
} }
export function AddUserModal({ isOpen, onClose, type, requestId, requestTitle }: AddUserModalProps) { export function AddUserModal({ isOpen, onClose, type, requestTitle }: AddUserModalProps) {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Textarea } from '../ui/textarea'; import { Textarea } from '../ui/textarea';

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Label } from '../ui/label'; import { Label } from '../ui/label';

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Input } from '../ui/input'; import { Input } from '../ui/input';
@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Avatar, AvatarFallback } from '../ui/avatar'; import { Avatar, AvatarFallback } from '../ui/avatar';
import { Progress } from '../ui/progress'; import { Progress } from '../ui/progress';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Switch } from '../ui/switch'; import { Switch } from '../ui/switch';
import { Calendar } from '../ui/calendar'; import { Calendar } from '../ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
@ -18,8 +18,6 @@ import {
Calendar as CalendarIcon, Calendar as CalendarIcon,
Upload, Upload,
X, X,
User,
Clock,
FileText, FileText,
Check, Check,
Users Users

View File

@ -1,15 +1,12 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../ui/dialog';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Badge } from '../ui/badge'; import { Badge } from '../ui/badge';
import { Separator } from '../ui/separator'; import { Separator } from '../ui/separator';
import { import {
FileText,
Receipt, Receipt,
Package, Package,
TrendingUp,
Users,
ArrowRight, ArrowRight,
Clock, Clock,
CheckCircle, CheckCircle,

View File

@ -38,7 +38,6 @@ interface WorkNoteModalProps {
export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps) { export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps) {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const participants = [ const participants = [
@ -139,11 +138,13 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)
const extractMentions = (text: string): string[] => { const extractMentions = (text: string): string[] => {
const mentionRegex = /@(\w+\s?\w+)/g; const mentionRegex = /@(\w+\s?\w+)/g;
const mentions = []; const mentions: string[] = [];
let match; let match;
while ((match = mentionRegex.exec(text)) !== null) { while ((match = mentionRegex.exec(text)) !== null) {
if (match[1]) {
mentions.push(match[1]); mentions.push(match[1]);
} }
}
return mentions; return mentions;
}; };
@ -230,23 +231,6 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)
</div> </div>
</div> </div>
))} ))}
{isTyping && (
<div className="flex gap-3">
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-gray-400 text-white text-xs">
...
</AvatarFallback>
</Avatar>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="flex gap-1">
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
Someone is typing...
</div>
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
</ScrollArea> </ScrollArea>

View File

@ -30,8 +30,6 @@ export function AddApproverModal({
open, open,
onClose, onClose,
onConfirm, onConfirm,
requestIdDisplay,
requestTitle,
existingParticipants = [], existingParticipants = [],
currentLevels = [] currentLevels = []
}: AddApproverModalProps) { }: AddApproverModalProps) {

View File

@ -19,8 +19,6 @@ export function AddSpectatorModal({
open, open,
onClose, onClose,
onConfirm, onConfirm,
requestIdDisplay,
requestTitle,
existingParticipants = [] existingParticipants = []
}: AddSpectatorModalProps) { }: AddSpectatorModalProps) {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');

View File

@ -72,7 +72,6 @@ interface Participant {
interface WorkNoteChatProps { interface WorkNoteChatProps {
requestId: string; requestId: string;
onBack?: () => void;
messages?: any[]; // optional external messages messages?: any[]; // optional external messages
onSend?: (messageHtml: string, files: File[]) => Promise<void> | void; onSend?: (messageHtml: string, files: File[]) => Promise<void> | void;
skipSocketJoin?: boolean; // Set to true when embedded in RequestDetail (to avoid double join) skipSocketJoin?: boolean; // Set to true when embedded in RequestDetail (to avoid double join)
@ -130,7 +129,7 @@ const FileIcon = ({ type }: { type: string }) => {
return <Paperclip className={`${iconClass} text-gray-600`} />; return <Paperclip className={`${iconClass} text-gray-600`} />;
}; };
export function WorkNoteChat({ requestId, onBack, 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, currentLevels = [], onAddApprover }: WorkNoteChatProps) {
const routeParams = useParams<{ requestId: string }>(); const routeParams = useParams<{ requestId: string }>();
const effectiveRequestId = requestId || routeParams.requestId || ''; const effectiveRequestId = requestId || routeParams.requestId || '';
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
@ -177,7 +176,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
}, [effectiveRequestId, requestTitle]); }, [effectiveRequestId, requestTitle]);
const [participants, setParticipants] = useState<Participant[]>([]); const [participants, setParticipants] = useState<Participant[]>([]);
const [loadingMessages, setLoadingMessages] = useState(false);
const onlineParticipants = participants.filter(p => p.status === 'online'); const onlineParticipants = participants.filter(p => p.status === 'online');
const filteredMessages = messages.filter(msg => const filteredMessages = messages.filter(msg =>
msg.content.toLowerCase().includes(searchTerm.toLowerCase()) || msg.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
@ -200,7 +198,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
(async () => { (async () => {
try { try {
setLoadingMessages(true);
const rows = await getWorkNotes(effectiveRequestId); const rows = await getWorkNotes(effectiveRequestId);
const mapped = Array.isArray(rows) ? rows.map((m: any) => { const mapped = Array.isArray(rows) ? rows.map((m: any) => {
const noteUserId = m.userId || m.user_id; const noteUserId = m.userId || m.user_id;
@ -229,8 +226,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
console.log(`[WorkNoteChat] Loaded ${mapped.length} messages from backend`); console.log(`[WorkNoteChat] Loaded ${mapped.length} messages from backend`);
} catch (error) { } catch (error) {
console.error('[WorkNoteChat] Failed to load messages:', error); console.error('[WorkNoteChat] Failed to load messages:', error);
} finally {
setLoadingMessages(false);
} }
})(); })();
}, [effectiveRequestId, currentUserId, externalMessages]); }, [effectiveRequestId, currentUserId, externalMessages]);
@ -442,12 +437,8 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
} }
} catch {} } catch {}
try { try {
// Get backend URL from environment (same as API calls) // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL)
// Strip /api/v1 suffix if present to get base WebSocket URL const s = getSocket(); // Uses getSocketBaseUrl() helper internally
const apiBaseUrl = (import.meta as any).env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1';
const base = apiBaseUrl.replace(/\/api\/v1$/, '');
console.log('[WorkNoteChat] Connecting socket to:', base);
const s = getSocket(base);
// Only join room if not skipped (standalone mode) // Only join room if not skipped (standalone mode)
if (!skipSocketJoin) { if (!skipSocketJoin) {

View File

@ -53,7 +53,6 @@ interface Message {
interface WorkNoteChatSimpleProps { interface WorkNoteChatSimpleProps {
requestId: string; requestId: string;
messages?: any[]; messages?: any[];
loading?: boolean;
onSend?: (messageHtml: string, files: File[]) => Promise<void> | void; onSend?: (messageHtml: string, files: File[]) => Promise<void> | void;
} }
@ -91,7 +90,7 @@ const formatParticipantRole = (role: string | undefined): string => {
} }
}; };
export function WorkNoteChatSimple({ requestId, messages: externalMessages, loading, onSend }: WorkNoteChatSimpleProps) { export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSend }: WorkNoteChatSimpleProps) {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@ -142,8 +141,8 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, load
} }
} catch {} } catch {}
try { try {
const base = (import.meta as any).env.VITE_BASE_URL || window.location.origin; // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL)
const s = getSocket(base); const s = getSocket(); // Uses getSocketBaseUrl() helper internally
joinRequestRoom(s, joinedId, currentUserId || undefined); joinRequestRoom(s, joinedId, currentUserId || undefined);
@ -329,18 +328,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, load
'🚀', '🎯', '🔍', '🔔', '💡' '🚀', '🎯', '🔍', '🔔', '💡'
]; ];
const extractMentions = (text: string): string[] => {
const mentionRegex = /@([\w\s]+)(?=\s|$|[.,!?])/g;
const mentions: string[] = [];
let match;
while ((match = mentionRegex.exec(text)) !== null) {
if (match[1]) {
mentions.push(match[1].trim());
}
}
return mentions;
};
const handleKeyPress = (e: React.KeyboardEvent) => { const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -22,11 +22,10 @@ import {
CheckCircle, CheckCircle,
Info, Info,
FileText, FileText,
DollarSign
} from 'lucide-react'; } from 'lucide-react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { getAllDealers, getDealerInfo, formatDealerAddress, type DealerInfo } from '@/utils/dealerDatabase'; import { getAllDealers, getDealerInfo, formatDealerAddress } from '@/utils/dealerDatabase';
interface ClaimManagementWizardProps { interface ClaimManagementWizardProps {
onBack?: () => void; onBack?: () => void;
@ -590,7 +589,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
<div className="mt-4 sm:mt-6"> <div className="mt-4 sm:mt-6">
<Progress value={(currentStep / totalSteps) * 100} className="h-2" /> <Progress value={(currentStep / totalSteps) * 100} className="h-2" />
<div className="flex justify-between mt-2 px-1"> <div className="flex justify-between mt-2 px-1">
{STEP_NAMES.map((name, index) => ( {STEP_NAMES.map((_name, index) => (
<span <span
key={index} key={index}
className={`text-xs sm:text-sm ${ className={`text-xs sm:text-sm ${

View File

@ -45,8 +45,10 @@ interface AuthProviderProps {
/** /**
* Check if running on localhost * Check if running on localhost
* Note: Function reserved for future use
* @internal - Reserved for future use
*/ */
const isLocalhost = (): boolean => { export const _isLocalhost = (): boolean => {
return ( return (
window.location.hostname === 'localhost' || window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' || window.location.hostname === '127.0.0.1' ||
@ -510,8 +512,10 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
/** /**
* Auth0-based Auth Provider (for production) * Auth0-based Auth Provider (for production)
* Note: Reserved for future use when Auth0 integration is needed
* @internal - Reserved for future use
*/ */
function Auth0AuthProvider({ children }: { children: ReactNode }) { export function _Auth0AuthProvider({ children }: { children: ReactNode }) {
return ( return (
<Auth0Provider <Auth0Provider
domain="https://dev-830839.oktapreview.com/oauth2/default/v1" domain="https://dev-830839.oktapreview.com/oauth2/default/v1"
@ -531,6 +535,7 @@ function Auth0AuthProvider({ children }: { children: ReactNode }) {
); );
} }
/** /**
* Wrapper to convert Auth0 hook to our context format * Wrapper to convert Auth0 hook to our context format
*/ */

View File

@ -184,7 +184,9 @@ export function useDocumentUpload(
// API Call: Upload document to backend // API Call: Upload document to backend
// Document type is 'SUPPORTING' (as opposed to 'REQUIRED') // Document type is 'SUPPORTING' (as opposed to 'REQUIRED')
if (file) {
await uploadDocument(file, requestId, 'SUPPORTING'); await uploadDocument(file, requestId, 'SUPPORTING');
}
// Refresh: Reload request details to show newly uploaded document // Refresh: Reload request details to show newly uploaded document
// This also updates the activity timeline // This also updates the activity timeline

View File

@ -73,8 +73,7 @@ export function useRequestSocket(
if (!mounted) return; if (!mounted) return;
// Initialize: Get socket instance with base URL // Initialize: Get socket instance with base URL
const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; const socket = getSocket(); // Uses getSocketBaseUrl() helper internally
const socket = getSocket(baseUrl);
if (!socket) { if (!socket) {
console.error('[useRequestSocket] Socket not available'); console.error('[useRequestSocket] Socket not available');
@ -159,8 +158,8 @@ export function useRequestSocket(
useEffect(() => { useEffect(() => {
if (!requestIdentifier) return; if (!requestIdentifier) return;
const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL)
const socket = getSocket(baseUrl); const socket = getSocket(); // Uses getSocketBaseUrl() helper internally
if (!socket) return; if (!socket) return;

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
@ -116,7 +116,7 @@ export function AuthCallback() {
</div> </div>
{/* Progress Steps */} {/* Progress Steps */}
{authStep !== 'error' && authStep !== 'complete' && ( {authStep !== 'error' && (
<div className="space-y-3 mb-6"> <div className="space-y-3 mb-6">
<div className={`flex items-center gap-3 text-sm transition-all duration-500 ${authStep === 'exchanging' ? 'text-white' : 'text-slate-400'}`}> <div className={`flex items-center gap-3 text-sm transition-all duration-500 ${authStep === 'exchanging' ? 'text-white' : 'text-slate-400'}`}>
<div className={`w-2 h-2 rounded-full transition-all duration-500 ${authStep === 'exchanging' ? 'bg-orange-500 animate-pulse' : 'bg-slate-600'}`}></div> <div className={`w-2 h-2 rounded-full transition-all duration-500 ${authStep === 'exchanging' ? 'bg-orange-500 animate-pulse' : 'bg-slate-600'}`}></div>
@ -126,10 +126,12 @@ export function AuthCallback() {
<div className={`w-2 h-2 rounded-full transition-all duration-500 ${authStep === 'fetching' ? 'bg-orange-500 animate-pulse' : 'bg-slate-600'}`}></div> <div className={`w-2 h-2 rounded-full transition-all duration-500 ${authStep === 'fetching' ? 'bg-orange-500 animate-pulse' : 'bg-slate-600'}`}></div>
<span>Loading your profile</span> <span>Loading your profile</span>
</div> </div>
<div className={`flex items-center gap-3 text-sm transition-all duration-500 ${authStep === 'complete' ? 'text-white' : 'text-slate-400'}`}> {authStep === 'complete' && (
<div className={`w-2 h-2 rounded-full transition-all duration-500 ${authStep === 'complete' ? 'bg-green-500' : 'bg-slate-600'}`}></div> <div className="flex items-center gap-3 text-sm transition-all duration-500 text-white">
<div className="w-2 h-2 rounded-full transition-all duration-500 bg-green-500"></div>
<span>Setting up your session</span> <span>Setting up your session</span>
</div> </div>
)}
</div> </div>
)} )}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { Auth } from './Auth'; import { Auth } from './Auth';
import { AuthCallback } from './AuthCallback'; import { AuthCallback } from './AuthCallback';

View File

@ -19,7 +19,6 @@ import {
AlertCircle AlertCircle
} from 'lucide-react'; } from 'lucide-react';
import { dashboardService } from '@/services/dashboard.service'; import { dashboardService } from '@/services/dashboard.service';
import { useAuth } from '@/contexts/AuthContext';
interface DetailedReportsProps { interface DetailedReportsProps {
onBack?: () => void; onBack?: () => void;
@ -27,7 +26,6 @@ interface DetailedReportsProps {
export function DetailedReports({ onBack }: DetailedReportsProps) { export function DetailedReports({ onBack }: DetailedReportsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth();
const [threshold, setThreshold] = useState('7'); const [threshold, setThreshold] = useState('7');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@ -88,15 +86,6 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
navigate(`/request/${requestId}`); navigate(`/request/${requestId}`);
}; };
// Helper function to calculate calendar days between dates
const calculateDaysOpen = (startDate: string | null | undefined): number => {
if (!startDate) return 0;
const start = new Date(startDate);
const now = new Date();
const diffTime = Math.abs(now.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
// Helper function to format TAT hours (working hours to working days) // Helper function to format TAT hours (working hours to working days)
// Backend returns working hours, so we divide by 8 (working hours per day) not 24 // Backend returns working hours, so we divide by 8 (working hours per day) not 24
@ -144,7 +133,7 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
}; };
// Helper function to map activity type to display label // Helper function to map activity type to display label
const mapActivityType = (type: string, activityDescription?: string): string => { const mapActivityType = (type: string, _activityDescription?: string): string => {
const typeLower = (type || '').toLowerCase(); const typeLower = (type || '').toLowerCase();
if (typeLower.includes('created') || typeLower.includes('create')) return 'Created Request'; if (typeLower.includes('created') || typeLower.includes('create')) return 'Created Request';
if (typeLower.includes('approval') || typeLower.includes('approved')) return 'Approved Request'; if (typeLower.includes('approval') || typeLower.includes('approved')) return 'Approved Request';

View File

@ -2,7 +2,6 @@ import { useAuth, isAdmin, isManagement } from '@/contexts/AuthContext';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { import {
User, User,
Mail, Mail,
@ -11,7 +10,6 @@ import {
Phone, Phone,
Shield, Shield,
Calendar, Calendar,
Edit,
CheckCircle, CheckCircle,
Users Users
} from 'lucide-react'; } from 'lucide-react';

View File

@ -6,10 +6,6 @@ export function WorkNotes() {
const { requestId } = useParams<{ requestId: string }>(); const { requestId } = useParams<{ requestId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
const handleBack = () => {
navigate(`/request/${requestId}`);
};
const handleNavigate = (page: string) => { const handleNavigate = (page: string) => {
navigate(`/${page}`); navigate(`/${page}`);
}; };
@ -33,7 +29,6 @@ export function WorkNotes() {
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<WorkNoteChat <WorkNoteChat
requestId={requestId || ''} requestId={requestId || ''}
onBack={handleBack}
/> />
</div> </div>
</PageLayout> </PageLayout>

View File

@ -1,23 +1,9 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { authSlice } from './slices/authSlice'; import authSlice from './slices/authSlice';
import { workflowSlice } from './slices/workflowSlice';
import { approvalSlice } from './slices/approvalSlice';
import { notificationSlice } from './slices/notificationSlice';
import { documentSlice } from './slices/documentSlice';
import { workNoteSlice } from './slices/workNoteSlice';
import { participantSlice } from './slices/participantSlice';
import { uiSlice } from './slices/uiSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
auth: authSlice.reducer, auth: authSlice.reducer,
workflow: workflowSlice.reducer,
approval: approvalSlice.reducer,
notification: notificationSlice.reducer,
document: documentSlice.reducer,
workNote: workNoteSlice.reducer,
participant: participantSlice.reducer,
ui: uiSlice.reducer,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({

View File

@ -272,9 +272,8 @@ export async function downloadDocument(documentId: string): Promise<void> {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const filename = contentDisposition const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, '');
? contentDisposition.split('filename=')[1]?.replace(/"/g, '') const filename: string = extractedFilename || 'download';
: 'download';
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = url; downloadLink.href = url;
@ -312,9 +311,8 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise<
// Get filename from Content-Disposition header or use default // Get filename from Content-Disposition header or use default
const contentDisposition = response.headers.get('Content-Disposition'); const contentDisposition = response.headers.get('Content-Disposition');
const filename = contentDisposition const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, '');
? contentDisposition.split('filename=')[1]?.replace(/"/g, '') const filename: string = extractedFilename || 'download';
: 'download';
const downloadLink = document.createElement('a'); const downloadLink = document.createElement('a');
downloadLink.href = url; downloadLink.href = url;
@ -388,7 +386,7 @@ export async function rejectLevel(requestId: string, levelId: string, rejectionR
return res.data?.data || res.data; return res.data?.data || res.data;
} }
export async function updateAndSubmitWorkflow(requestId: string, workflowData: CreateWorkflowFromFormPayload, files?: File[]) { export async function updateAndSubmitWorkflow(requestId: string, workflowData: CreateWorkflowFromFormPayload, _files?: File[]) {
// First update the workflow // First update the workflow
const payload: any = { const payload: any = {
title: workflowData.title, title: workflowData.title,

View File

@ -2,12 +2,36 @@ import { io, Socket } from 'socket.io-client';
let socket: Socket | null = null; let socket: Socket | null = null;
export function getSocket(baseUrl: string): Socket { /**
* Get the base URL for socket.io connection
* Uses VITE_BASE_URL if available, otherwise derives from VITE_API_BASE_URL
*/
export function getSocketBaseUrl(): string {
// Prefer VITE_BASE_URL (direct backend URL without /api/v1)
const baseUrl = import.meta.env.VITE_BASE_URL;
if (baseUrl) {
return baseUrl;
}
// Fallback: derive from VITE_API_BASE_URL by removing /api/v1
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
if (apiBaseUrl) {
return apiBaseUrl.replace(/\/api\/v1\/?$/, '');
}
// Development fallback
console.warn('[Socket] No VITE_BASE_URL or VITE_API_BASE_URL found, using localhost:5000');
return 'http://localhost:5000';
}
export function getSocket(baseUrl?: string): Socket {
// Use provided baseUrl or get from environment
const url = baseUrl || getSocketBaseUrl();
if (socket) return socket; if (socket) return socket;
console.log('[Socket] Connecting to:', baseUrl); console.log('[Socket] Connecting to:', url);
socket = io(baseUrl, { socket = io(url, {
withCredentials: true, withCredentials: true,
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
path: '/socket.io', path: '/socket.io',

View File

@ -30,6 +30,7 @@ export const cookieUtils = {
const cookies = document.cookie.split(';'); const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) { for (let i = 0; i < cookies.length; i++) {
let cookie = cookies[i]; let cookie = cookies[i];
if (!cookie) continue;
while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length); while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length);
if (cookie.indexOf(nameEQ) === 0) { if (cookie.indexOf(nameEQ) === 0) {
return decodeURIComponent(cookie.substring(nameEQ.length, cookie.length)); return decodeURIComponent(cookie.substring(nameEQ.length, cookie.length));
@ -398,7 +399,9 @@ export function isTokenExpired(token: string | null, bufferMinutes: number = 5):
if (!token) return true; if (!token) return true;
try { try {
const payload = JSON.parse(atob(token.split('.')[1])); const parts = token.split('.');
if (parts.length !== 3 || !parts[1]) return true;
const payload = JSON.parse(atob(parts[1]));
const exp = payload.exp * 1000; // Convert to milliseconds const exp = payload.exp * 1000; // Convert to milliseconds
const now = Date.now(); const now = Date.now();
const buffer = bufferMinutes * 60 * 1000; const buffer = bufferMinutes * 60 * 1000;
@ -415,7 +418,9 @@ export function getTokenExpiration(token: string | null): Date | null {
if (!token) return null; if (!token) return null;
try { try {
const payload = JSON.parse(atob(token.split('.')[1])); const parts = token.split('.');
if (parts.length !== 3 || !parts[1]) return null;
const payload = JSON.parse(atob(parts[1]));
return new Date(payload.exp * 1000); return new Date(payload.exp * 1000);
} catch { } catch {
return null; return null;

View File

@ -2,9 +2,67 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path from 'path'; import path from 'path';
// Plugin to suppress CSS minification warnings
const suppressCssWarnings = () => {
return {
name: 'suppress-css-warnings',
buildStart() {
// Intercept console.warn
const originalWarn = console.warn;
console.warn = (...args: any[]) => {
const message = args.join(' ');
// Suppress CSS syntax error warnings (known issue with Tailwind)
if (message.includes('css-syntax-error') || message.includes('Unexpected "header"')) {
return;
}
originalWarn.apply(console, args);
};
// Intercept process.stderr.write (where esbuild outputs warnings)
const originalStderrWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = (chunk: any, encoding?: any, callback?: any) => {
const message = chunk?.toString() || '';
// Suppress CSS syntax error warnings
if (message.includes('css-syntax-error') || message.includes('Unexpected "header"')) {
if (typeof callback === 'function') callback();
return true;
}
return originalStderrWrite(chunk, encoding, callback);
};
},
};
};
// Plugin to ensure proper chunk loading order
// React must load before Radix UI to prevent "Cannot access 'React' before initialization" errors
const ensureChunkOrder = () => {
return {
name: 'ensure-chunk-order',
generateBundle(options: any, bundle: any) {
// Find React vendor chunk and ensure it's loaded first
const reactChunk = Object.keys(bundle).find(
(key) => bundle[key].type === 'chunk' && bundle[key].name === 'react-vendor'
);
if (reactChunk) {
// Ensure Radix vendor chunk depends on React vendor chunk
Object.keys(bundle).forEach((key) => {
const chunk = bundle[key];
if (chunk.type === 'chunk' && chunk.name === 'radix-vendor') {
if (!chunk.imports) chunk.imports = [];
if (!chunk.imports.includes(reactChunk)) {
chunk.imports.push(reactChunk);
}
}
});
}
},
};
};
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react(), suppressCssWarnings(), ensureChunkOrder()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
@ -23,28 +81,117 @@ export default defineConfig({
build: { build: {
outDir: 'dist', outDir: 'dist',
sourcemap: true, sourcemap: true,
// CSS minification warning is harmless - it's a known issue with certain Tailwind class combinations
// Re-enable minification with settings that preserve initialization order
// The "Cannot access 'React' before initialization" error is fixed by keeping React in main bundle
minify: 'esbuild',
// Preserve class names to help with debugging and prevent initialization issues
terserOptions: undefined, // Use esbuild minifier instead
// Ensure proper module format to prevent circular dependency issues
modulePreload: { polyfill: true },
commonjsOptions: {
// Help resolve circular dependencies
include: [/node_modules/],
transformMixedEsModules: true,
},
rollupOptions: { rollupOptions: {
onwarn(warning, warn) {
// Suppress circular dependency warnings for known issues
if (warning.code === 'CIRCULAR_DEPENDENCY') {
// Allow circular deps between Radix UI packages (they handle it internally)
if (warning.message?.includes('@radix-ui') || warning.message?.includes('react-primitive')) {
return;
}
}
// Suppress CSS minification warnings (known issue with Tailwind class combinations)
if (warning.code === 'css-syntax-error' && warning.message?.includes('header')) {
return;
}
// Suppress all CSS syntax errors (they're usually false positives from minifier)
if (warning.code === 'css-syntax-error') {
return;
}
// Use default warning handler for other warnings
warn(warning);
},
output: { output: {
manualChunks: { // Preserve module structure to prevent circular dependency issues
'react-vendor': ['react', 'react-dom'], preserveModules: false,
'ui-vendor': ['lucide-react', 'sonner'], // Ensure proper chunk ordering and prevent circular dependency issues
'radix-vendor': [ chunkFileNames: 'assets/[name]-[hash].js',
'@radix-ui/react-accordion', // Explicitly define chunk order - React must load before Radix UI
'@radix-ui/react-avatar', manualChunks(id) {
'@radix-ui/react-dialog', // CRITICAL FIX: Keep React in main bundle OR ensure it loads first
'@radix-ui/react-dropdown-menu', // The "Cannot access 'React' before initialization" error occurs when
'@radix-ui/react-label', // Radix UI components try to access React before it's initialized
'@radix-ui/react-popover', // Option 1: Don't split React - keep it in main bundle (most reliable)
'@radix-ui/react-select', // Option 2: Keep React in separate chunk but ensure it loads first
'@radix-ui/react-tabs',
], // For now, let's keep React in main bundle to avoid initialization issues
// Only split other vendors
// Radix UI - CRITICAL: ALL Radix packages MUST stay together in ONE chunk
// This chunk will import React from the main bundle, avoiding initialization issues
if (id.includes('node_modules/@radix-ui')) {
return 'radix-vendor';
}
// Don't split React - keep it in main bundle to ensure proper initialization
// This prevents "Cannot access 'React' before initialization" errors
// UI libraries
if (id.includes('node_modules/lucide-react') || id.includes('node_modules/sonner')) {
return 'ui-vendor';
}
// Router
if (id.includes('node_modules/react-router')) {
return 'router-vendor';
}
// Redux
if (id.includes('node_modules/@reduxjs') || id.includes('node_modules/redux')) {
return 'redux-vendor';
}
// Socket.io
if (id.includes('node_modules/socket.io')) {
return 'socket-vendor';
}
// Large charting library
if (id.includes('node_modules/recharts')) {
return 'charts-vendor';
}
// Other large dependencies
if (id.includes('node_modules/axios') || id.includes('node_modules/date-fns')) {
return 'utils-vendor';
}
// Don't split our own code into chunks to avoid circular deps
},
entryFileNames: 'assets/[name]-[hash].js',
// Use ES format but ensure proper chunk dependencies
format: 'es',
// Ensure proper chunk dependency order
// React vendor must be loaded before Radix vendor
// This is handled by the ensureChunkOrder plugin
}, },
}, },
}, chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks
chunkSizeWarningLimit: 1000,
}, },
optimizeDeps: { optimizeDeps: {
include: ['react', 'react-dom', 'lucide-react'], include: [
'react',
'react-dom',
'lucide-react',
// Pre-bundle Radix UI components together to prevent circular deps
'@radix-ui/react-label',
'@radix-ui/react-slot',
'@radix-ui/react-primitive',
],
// Ensure Radix UI components are pre-bundled together
esbuildOptions: {
// Prevent circular dependency issues
keepNames: true,
// Target ES2020 for better module handling
target: 'es2020',
// Preserve module initialization order
preserveSymlinks: false,
},
}, },
}); });