diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..846520e --- /dev/null +++ b/.env.example @@ -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}} diff --git a/Attributions.md b/Attributions.md deleted file mode 100644 index 9b7cd4e..0000000 --- a/Attributions.md +++ /dev/null @@ -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). \ No newline at end of file diff --git a/DetailedReports_Analysis.md b/DetailedReports_Analysis.md deleted file mode 100644 index 2282c24..0000000 --- a/DetailedReports_Analysis.md +++ /dev/null @@ -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 - diff --git a/README.md b/README.md index 1485e1e..509aa7c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,15 @@ A modern, enterprise-grade approval and request management system built with Rea ## ๐Ÿš€ 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 \`\`\`bash @@ -76,36 +85,114 @@ npm install ### 3. Set up environment variables +#### Option A: Automated Setup (Recommended - Unix/Linux/Mac) + +Run the setup script to automatically create environment files: + \`\`\`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 -VITE_API_BASE_URL=http://localhost:5000/api -VITE_APP_NAME=Royal Enfield Approval Portal +# Local Development Environment +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 -# Create src directory structure -mkdir -p src/components src/utils src/styles src/types - -# Move existing files (you'll need to do this manually or run the migration script) -# The structure should match the project structure below +# Check environment file exists +ls -la .env.local # Unix/Linux/Mac +# or +Test-Path .env.local # Windows PowerShell \`\`\` ## ๐Ÿ’ป 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 \`\`\`bash 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 @@ -183,6 +270,10 @@ import { Button } from '@/components/ui/button'; import { getDealerInfo } from '@/utils/dealerDatabase'; \`\`\` +Path aliases are configured in: +- `tsconfig.json` - TypeScript path mapping +- `vite.config.ts` - Vite resolver configuration + ### Tailwind CSS Customization 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: \`\`\`typescript +// Access environment variables 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 -# Move App.tsx -mv App.tsx src/ +# Option 1: Kill the process using the port +# Windows +netstat -ano | findstr :5173 +taskkill /PID /F -# Move components -mv components src/ +# Unix/Linux/Mac +lsof -ti:5173 | xargs kill -9 -# Move utils -mv utils src/ - -# Move styles -mv styles src/ +# Option 2: Use a different port +npm run dev -- --port 3000 \`\`\` -### 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 -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; -import './styles/globals.css'; +#### Backend Connection Issues -ReactDOM.createRoot(document.getElementById('root')!).render( - - - -); +1. Verify backend is running on the configured port +2. Check `VITE_API_BASE_URL` in `.env.local` matches backend URL +3. Ensure CORS is configured in backend to allow frontend origin +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: - -\`\`\`typescript -// Before -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 +- Check browser console for errors +- Verify all environment variables are set correctly +- Ensure Node.js and npm versions meet requirements +- Review backend logs for API-related issues ## ๐Ÿงช Testing (Future Enhancement) diff --git a/ROLE_MIGRATION.md b/ROLE_MIGRATION.md deleted file mode 100644 index aa0ebb2..0000000 --- a/ROLE_MIGRATION.md +++ /dev/null @@ -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! - diff --git a/USER_ROLE_MANAGEMENT.md b/USER_ROLE_MANAGEMENT.md deleted file mode 100644 index 998c50b..0000000 --- a/USER_ROLE_MANAGEMENT.md +++ /dev/null @@ -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! ๐ŸŽฏ - diff --git a/package-lock.json b/package-lock.json index 5e59409..77ec0a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,9 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-redux": "^9.2.0", + "react-resizable-panels": "^2.1.0", "react-router-dom": "^7.9.4", "recharts": "^2.13.3", "socket.io-client": "^4.8.1", @@ -6117,6 +6119,22 @@ "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": { "version": "18.3.1", "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": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", diff --git a/package.json b/package.json index 5f46ca4..9962146 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "react-redux": "^9.2.0", + "react-resizable-panels": "^2.1.0", "react-router-dom": "^7.9.4", "recharts": "^2.13.3", "socket.io-client": "^4.8.1", diff --git a/setup-env.bat b/setup-env.bat deleted file mode 100644 index 1e036b9..0000000 --- a/setup-env.bat +++ /dev/null @@ -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 - diff --git a/setup-env.sh b/setup-env.sh index 48dc65a..e541ea8 100644 --- a/setup-env.sh +++ b/setup-env.sh @@ -15,6 +15,13 @@ VITE_API_BASE_URL=http://localhost:5000/api/v1 # Base URL for direct file access (without /api/v1) 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 echo "โœ… Created .env.example" } @@ -25,6 +32,13 @@ create_env_local() { # Local Development Environment VITE_API_BASE_URL=http://localhost:5000/api/v1 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 echo "โœ… Created .env.local (for local development)" } @@ -37,25 +51,55 @@ create_env_production() { echo "==================================================" echo "" 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 echo "โš ๏ธ No backend URL provided. Creating template file..." cat > .env.production << 'EOF' # 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_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 else - # Remove trailing slash if present - BACKEND_URL=${BACKEND_URL%/} - cat > .env.production << EOF # Production Environment + +# API Configuration VITE_API_BASE_URL=${BACKEND_URL}/api/v1 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 - 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 } @@ -99,11 +143,7 @@ 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 "3. Update Backend CORS:" echo " - Add production frontend URL to CORS allowed origins" echo "" echo "๐Ÿ“– For detailed instructions, see: DEPLOYMENT_CONFIGURATION.md" diff --git a/src/App.tsx b/src/App.tsx index fba7209..93ba1cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -239,57 +239,6 @@ function AppRoutes({ onLogout }: AppProps) { 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) => { // Generate unique ID for the new claim request const requestId = `RE-REQ-2024-CM-${String(dynamicRequests.length + 2).padStart(3, '0')}`; diff --git a/src/components/Auth/AuthDebugInfo.tsx b/src/components/Auth/AuthDebugInfo.tsx index 90e4049..d754b98 100644 --- a/src/components/Auth/AuthDebugInfo.tsx +++ b/src/components/Auth/AuthDebugInfo.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; diff --git a/src/components/admin/AIConfig/AIConfig.tsx b/src/components/admin/AIConfig/AIConfig.tsx index d818223..cc490a6 100644 --- a/src/components/admin/AIConfig/AIConfig.tsx +++ b/src/components/admin/AIConfig/AIConfig.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; 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 { AIFeatures } from './AIFeatures'; import { AIParameters } from './AIParameters'; diff --git a/src/components/admin/AIConfig/AIProviderSettings.tsx b/src/components/admin/AIConfig/AIProviderSettings.tsx index 81f71ea..3e25560 100644 --- a/src/components/admin/AIConfig/AIProviderSettings.tsx +++ b/src/components/admin/AIConfig/AIProviderSettings.tsx @@ -49,28 +49,6 @@ export function AIProviderSettings({ onToggleApiKeyVisibility, maskApiKey }: 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 ( diff --git a/src/components/admin/ConfigurationManager.tsx b/src/components/admin/ConfigurationManager.tsx index 0fdb74a..5a9fb1a 100644 --- a/src/components/admin/ConfigurationManager.tsx +++ b/src/components/admin/ConfigurationManager.tsx @@ -153,7 +153,6 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro const renderConfigInput = (config: AdminConfiguration) => { const currentValue = getCurrentValue(config); - const isChanged = hasChanges(config); const isSaving = saving === config.configKey; if (!config.isEditable) { @@ -203,7 +202,11 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro min={min} max={max} step={1} - onValueChange={([value]) => handleValueChange(config.configKey, value.toString())} + onValueChange={([value]) => { + if (value !== undefined) { + handleValueChange(config.configKey, value.toString()); + } + }} disabled={isSaving} className="w-full" /> @@ -276,13 +279,16 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro if (!acc[config.configCategory]) { acc[config.configCategory] = []; } - acc[config.configCategory].push(config); + acc[config.configCategory]!.push(config); return acc; }, {} as Record); // Sort configs within each category by sortOrder 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) { @@ -370,13 +376,13 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro {category.replace(/_/g, ' ')} - {groupedConfigs[category].length} setting{groupedConfigs[category].length !== 1 ? 's' : ''} available + {groupedConfigs[category]?.length || 0} setting{(groupedConfigs[category]?.length || 0) !== 1 ? 's' : ''} available - {groupedConfigs[category].map(config => ( + {groupedConfigs[category]?.map(config => (
diff --git a/src/components/admin/DashboardConfig/DashboardConfig.tsx b/src/components/admin/DashboardConfig/DashboardConfig.tsx index ad671e4..b030bd3 100644 --- a/src/components/admin/DashboardConfig/DashboardConfig.tsx +++ b/src/components/admin/DashboardConfig/DashboardConfig.tsx @@ -92,7 +92,7 @@ export function DashboardConfig() { handleKPIToggle(role, kpi, checked)} /> ))} diff --git a/src/components/admin/HolidayManager.tsx b/src/components/admin/HolidayManager.tsx index 8e3ef0d..8b33923 100644 --- a/src/components/admin/HolidayManager.tsx +++ b/src/components/admin/HolidayManager.tsx @@ -27,7 +27,6 @@ import { Loader2, AlertCircle, CheckCircle, - Upload } from 'lucide-react'; import { getAllHolidays, createHoliday, updateHoliday, deleteHoliday, Holiday } from '@/services/adminApi'; import { formatDateShort } from '@/utils/dateFormatter'; @@ -267,7 +266,7 @@ export function HolidayManager() {
{month} {selectedYear} - {holidaysByMonth[month].length} holiday{holidaysByMonth[month].length !== 1 ? 's' : ''} + {holidaysByMonth[month]?.length || 0} holiday{(holidaysByMonth[month]?.length || 0) !== 1 ? 's' : ''}
@@ -276,7 +275,7 @@ export function HolidayManager() {
- {holidaysByMonth[month].map(holiday => ( + {holidaysByMonth[month]?.map(holiday => (
onReminderThreshold1Change(value)} + onValueChange={([value]) => { + if (value !== undefined) { + onReminderThreshold1Change(value); + } + }} className="w-full" />

@@ -64,7 +68,11 @@ export function EscalationSettings({ min={1} max={100} step={1} - onValueChange={([value]) => onReminderThreshold2Change(value)} + onValueChange={([value]) => { + if (value !== undefined) { + onReminderThreshold2Change(value); + } + }} className="w-full" />

diff --git a/src/components/admin/UserManagement/UserManagement.tsx b/src/components/admin/UserManagement/UserManagement.tsx index 48a4dd5..a24192a 100644 --- a/src/components/admin/UserManagement/UserManagement.tsx +++ b/src/components/admin/UserManagement/UserManagement.tsx @@ -2,7 +2,6 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, @@ -15,15 +14,11 @@ import { Search, Users, Shield, - UserCog, Loader2, CheckCircle, AlertCircle, Crown, User as UserIcon, - Edit, - Trash2, - Power } from 'lucide-react'; import { userApi } from '@/services/userApi'; import { toast } from 'sonner'; @@ -355,27 +350,6 @@ export function UserManagement() { }; }, [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 ; - case 'MANAGEMENT': - return ; - default: - return ; - } - }; // Calculate stats for UserStatsCards const stats = { diff --git a/src/components/approval/ApprovalModal/ApprovalModal.tsx b/src/components/approval/ApprovalModal/ApprovalModal.tsx index 5c1545e..f785f23 100644 --- a/src/components/approval/ApprovalModal/ApprovalModal.tsx +++ b/src/components/approval/ApprovalModal/ApprovalModal.tsx @@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from ' import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; -import { CheckCircle, AlertCircle } from 'lucide-react'; +import { CheckCircle } from 'lucide-react'; type ApprovalModalProps = { open: boolean; diff --git a/src/components/approval/SkipApproverModal/SkipApproverModal.tsx b/src/components/approval/SkipApproverModal/SkipApproverModal.tsx index 2646df4..4c2a372 100644 --- a/src/components/approval/SkipApproverModal/SkipApproverModal.tsx +++ b/src/components/approval/SkipApproverModal/SkipApproverModal.tsx @@ -1,5 +1,5 @@ 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 { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index 2dda6fe..3d76369 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { Component, ErrorInfo, ReactNode } from 'react'; import { Button } from '@/components/ui/button'; import { AlertTriangle, RefreshCw, ArrowLeft } from 'lucide-react'; @@ -32,7 +32,7 @@ export class ErrorBoundary extends Component { }; } - componentDidCatch(error: Error, errorInfo: ErrorInfo) { + override componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('Error Boundary caught an error:', error, errorInfo); this.setState({ @@ -54,7 +54,7 @@ export class ErrorBoundary extends Component { }); }; - render() { + override render() { if (this.state.hasError) { // Custom fallback if provided if (this.props.fallback) { diff --git a/src/components/common/Loader/Loader.tsx b/src/components/common/Loader/Loader.tsx index c7fe225..535bd6c 100644 --- a/src/components/common/Loader/Loader.tsx +++ b/src/components/common/Loader/Loader.tsx @@ -1,4 +1,3 @@ -import React from 'react'; interface LoaderProps { message?: string; diff --git a/src/components/layout/PageLayout/PageLayout.tsx b/src/components/layout/PageLayout/PageLayout.tsx index 9de3d0a..263ff1a 100644 --- a/src/components/layout/PageLayout/PageLayout.tsx +++ b/src/components/layout/PageLayout/PageLayout.tsx @@ -1,5 +1,5 @@ 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 { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -143,8 +143,7 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on fetchNotifications(); // Setup socket for real-time notifications - const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; - const socket = getSocket(baseUrl); + const socket = getSocket(); // Uses getSocketBaseUrl() helper internally if (socket) { // Join user's personal notification room diff --git a/src/components/modals/AddUserModal.tsx b/src/components/modals/AddUserModal.tsx index 5d84a6d..15f59cf 100644 --- a/src/components/modals/AddUserModal.tsx +++ b/src/components/modals/AddUserModal.tsx @@ -10,11 +10,10 @@ interface AddUserModalProps { isOpen: boolean; onClose: () => void; type: 'approver' | 'spectator'; - requestId: 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 [isSubmitting, setIsSubmitting] = useState(false); diff --git a/src/components/modals/ApprovalActionModal.tsx b/src/components/modals/ApprovalActionModal.tsx index 2d73dcb..a0d058e 100644 --- a/src/components/modals/ApprovalActionModal.tsx +++ b/src/components/modals/ApprovalActionModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Button } from '../ui/button'; import { Textarea } from '../ui/textarea'; diff --git a/src/components/modals/DealerDocumentModal.tsx b/src/components/modals/DealerDocumentModal.tsx index 9ac40c7..a3848d8 100644 --- a/src/components/modals/DealerDocumentModal.tsx +++ b/src/components/modals/DealerDocumentModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'; import { Button } from '../ui/button'; import { Label } from '../ui/label'; diff --git a/src/components/modals/NewRequestModal.tsx b/src/components/modals/NewRequestModal.tsx index e67d3a6..a3f3dff 100644 --- a/src/components/modals/NewRequestModal.tsx +++ b/src/components/modals/NewRequestModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; @@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '. import { Badge } from '../ui/badge'; import { Avatar, AvatarFallback } from '../ui/avatar'; 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 { Calendar } from '../ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; @@ -18,8 +18,6 @@ import { Calendar as CalendarIcon, Upload, X, - User, - Clock, FileText, Check, Users diff --git a/src/components/modals/TemplateSelectionModal.tsx b/src/components/modals/TemplateSelectionModal.tsx index eba1061..53206fd 100644 --- a/src/components/modals/TemplateSelectionModal.tsx +++ b/src/components/modals/TemplateSelectionModal.tsx @@ -1,15 +1,12 @@ -import React, { useState } from 'react'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog'; +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../ui/dialog'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; import { Separator } from '../ui/separator'; import { - FileText, Receipt, Package, - TrendingUp, - Users, ArrowRight, Clock, CheckCircle, diff --git a/src/components/modals/WorkNoteModal.tsx b/src/components/modals/WorkNoteModal.tsx index 4e6ef58..f7cf4f0 100644 --- a/src/components/modals/WorkNoteModal.tsx +++ b/src/components/modals/WorkNoteModal.tsx @@ -38,7 +38,6 @@ interface WorkNoteModalProps { export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps) { const [message, setMessage] = useState(''); - const [isTyping, setIsTyping] = useState(false); const messagesEndRef = useRef(null); const participants = [ @@ -139,10 +138,12 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps) const extractMentions = (text: string): string[] => { const mentionRegex = /@(\w+\s?\w+)/g; - const mentions = []; + const mentions: string[] = []; let match; while ((match = mentionRegex.exec(text)) !== null) { - mentions.push(match[1]); + if (match[1]) { + mentions.push(match[1]); + } } return mentions; }; @@ -230,23 +231,6 @@ export function WorkNoteModal({ open, onClose, requestId }: WorkNoteModalProps)

))} - {isTyping && ( -
- - - ... - - -
-
-
-
-
-
- Someone is typing... -
-
- )}
diff --git a/src/components/participant/AddApproverModal/AddApproverModal.tsx b/src/components/participant/AddApproverModal/AddApproverModal.tsx index e05a7b2..1090702 100644 --- a/src/components/participant/AddApproverModal/AddApproverModal.tsx +++ b/src/components/participant/AddApproverModal/AddApproverModal.tsx @@ -30,8 +30,6 @@ export function AddApproverModal({ open, onClose, onConfirm, - requestIdDisplay, - requestTitle, existingParticipants = [], currentLevels = [] }: AddApproverModalProps) { diff --git a/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx b/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx index fd2e8d3..8c8f88d 100644 --- a/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx +++ b/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx @@ -19,8 +19,6 @@ export function AddSpectatorModal({ open, onClose, onConfirm, - requestIdDisplay, - requestTitle, existingParticipants = [] }: AddSpectatorModalProps) { const [email, setEmail] = useState(''); diff --git a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx index 6f9f3ef..ab4c24e 100644 --- a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx +++ b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx @@ -72,7 +72,6 @@ interface Participant { interface WorkNoteChatProps { requestId: string; - onBack?: () => void; messages?: any[]; // optional external messages onSend?: (messageHtml: string, files: File[]) => Promise | void; skipSocketJoin?: boolean; // Set to true when embedded in RequestDetail (to avoid double join) @@ -130,7 +129,7 @@ const FileIcon = ({ type }: { type: string }) => { return ; }; -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 effectiveRequestId = requestId || routeParams.requestId || ''; const [message, setMessage] = useState(''); @@ -177,7 +176,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on }, [effectiveRequestId, requestTitle]); const [participants, setParticipants] = useState([]); - const [loadingMessages, setLoadingMessages] = useState(false); const onlineParticipants = participants.filter(p => p.status === 'online'); const filteredMessages = messages.filter(msg => msg.content.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -200,7 +198,6 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on (async () => { try { - setLoadingMessages(true); const rows = await getWorkNotes(effectiveRequestId); const mapped = Array.isArray(rows) ? rows.map((m: any) => { 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`); } catch (error) { console.error('[WorkNoteChat] Failed to load messages:', error); - } finally { - setLoadingMessages(false); } })(); }, [effectiveRequestId, currentUserId, externalMessages]); @@ -442,12 +437,8 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on } } catch {} try { - // Get backend URL from environment (same as API calls) - // Strip /api/v1 suffix if present to get base WebSocket URL - 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); + // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL) + const s = getSocket(); // Uses getSocketBaseUrl() helper internally // Only join room if not skipped (standalone mode) if (!skipSocketJoin) { diff --git a/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx b/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx index a8ca2c3..013e582 100644 --- a/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx +++ b/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx @@ -53,7 +53,6 @@ interface Message { interface WorkNoteChatSimpleProps { requestId: string; messages?: any[]; - loading?: boolean; onSend?: (messageHtml: string, files: File[]) => Promise | 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 [searchTerm, setSearchTerm] = useState(''); const [showEmojiPicker, setShowEmojiPicker] = useState(false); @@ -142,8 +141,8 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, load } } catch {} try { - const base = (import.meta as any).env.VITE_BASE_URL || window.location.origin; - const s = getSocket(base); + // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL) + const s = getSocket(); // Uses getSocketBaseUrl() helper internally 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) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); diff --git a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx index 8bca9c4..a1a2432 100644 --- a/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx +++ b/src/components/workflow/ClaimManagementWizard/ClaimManagementWizard.tsx @@ -1,5 +1,5 @@ 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 { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; @@ -22,11 +22,10 @@ import { CheckCircle, Info, FileText, - DollarSign } from 'lucide-react'; import { format } from 'date-fns'; import { toast } from 'sonner'; -import { getAllDealers, getDealerInfo, formatDealerAddress, type DealerInfo } from '@/utils/dealerDatabase'; +import { getAllDealers, getDealerInfo, formatDealerAddress } from '@/utils/dealerDatabase'; interface ClaimManagementWizardProps { onBack?: () => void; @@ -590,7 +589,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
- {STEP_NAMES.map((name, index) => ( + {STEP_NAMES.map((_name, index) => ( { +export const _isLocalhost = (): boolean => { return ( window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || @@ -510,8 +512,10 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { /** * 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 ( { if (!requestIdentifier) return; - const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; - const socket = getSocket(baseUrl); + // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL) + const socket = getSocket(); // Uses getSocketBaseUrl() helper internally if (!socket) return; diff --git a/src/pages/Auth/AuthCallback.tsx b/src/pages/Auth/AuthCallback.tsx index 3641259..f0a7ac2 100644 --- a/src/pages/Auth/AuthCallback.tsx +++ b/src/pages/Auth/AuthCallback.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react'; @@ -116,7 +116,7 @@ export function AuthCallback() {
{/* Progress Steps */} - {authStep !== 'error' && authStep !== 'complete' && ( + {authStep !== 'error' && (
@@ -126,10 +126,12 @@ export function AuthCallback() {
Loading your profile
-
-
- Setting up your session -
+ {authStep === 'complete' && ( +
+
+ Setting up your session +
+ )}
)} diff --git a/src/pages/Auth/AuthenticatedApp.tsx b/src/pages/Auth/AuthenticatedApp.tsx index abbd7b7..0825975 100644 --- a/src/pages/Auth/AuthenticatedApp.tsx +++ b/src/pages/Auth/AuthenticatedApp.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { Auth } from './Auth'; import { AuthCallback } from './AuthCallback'; diff --git a/src/pages/DetailedReports/DetailedReports.tsx b/src/pages/DetailedReports/DetailedReports.tsx index e066e37..6769a0b 100644 --- a/src/pages/DetailedReports/DetailedReports.tsx +++ b/src/pages/DetailedReports/DetailedReports.tsx @@ -19,7 +19,6 @@ import { AlertCircle } from 'lucide-react'; import { dashboardService } from '@/services/dashboard.service'; -import { useAuth } from '@/contexts/AuthContext'; interface DetailedReportsProps { onBack?: () => void; @@ -27,7 +26,6 @@ interface DetailedReportsProps { export function DetailedReports({ onBack }: DetailedReportsProps) { const navigate = useNavigate(); - const { user } = useAuth(); const [threshold, setThreshold] = useState('7'); const [searchQuery, setSearchQuery] = useState(''); @@ -88,15 +86,6 @@ export function DetailedReports({ onBack }: DetailedReportsProps) { 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) // 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 - const mapActivityType = (type: string, activityDescription?: string): string => { + const mapActivityType = (type: string, _activityDescription?: string): string => { const typeLower = (type || '').toLowerCase(); if (typeLower.includes('created') || typeLower.includes('create')) return 'Created Request'; if (typeLower.includes('approval') || typeLower.includes('approved')) return 'Approved Request'; diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx index ad3dec6..cd03b26 100644 --- a/src/pages/Profile/Profile.tsx +++ b/src/pages/Profile/Profile.tsx @@ -2,7 +2,6 @@ import { useAuth, isAdmin, isManagement } from '@/contexts/AuthContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; import { User, Mail, @@ -11,7 +10,6 @@ import { Phone, Shield, Calendar, - Edit, CheckCircle, Users } from 'lucide-react'; diff --git a/src/pages/WorkNotes/WorkNotes.tsx b/src/pages/WorkNotes/WorkNotes.tsx index 1042559..aa855a6 100644 --- a/src/pages/WorkNotes/WorkNotes.tsx +++ b/src/pages/WorkNotes/WorkNotes.tsx @@ -6,10 +6,6 @@ export function WorkNotes() { const { requestId } = useParams<{ requestId: string }>(); const navigate = useNavigate(); - const handleBack = () => { - navigate(`/request/${requestId}`); - }; - const handleNavigate = (page: string) => { navigate(`/${page}`); }; @@ -33,7 +29,6 @@ export function WorkNotes() {
diff --git a/src/redux/store.ts b/src/redux/store.ts index 40d3f16..84423e7 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,23 +1,9 @@ import { configureStore } from '@reduxjs/toolkit'; -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'; +import authSlice from './slices/authSlice'; export const store = configureStore({ 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) => getDefaultMiddleware({ diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts index f9bee92..e9cee8e 100644 --- a/src/services/workflowApi.ts +++ b/src/services/workflowApi.ts @@ -272,9 +272,8 @@ export async function downloadDocument(documentId: string): Promise { const url = window.URL.createObjectURL(blob); const contentDisposition = response.headers.get('Content-Disposition'); - const filename = contentDisposition - ? contentDisposition.split('filename=')[1]?.replace(/"/g, '') - : 'download'; + const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, ''); + const filename: string = extractedFilename || 'download'; const downloadLink = document.createElement('a'); downloadLink.href = url; @@ -312,9 +311,8 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise< // Get filename from Content-Disposition header or use default const contentDisposition = response.headers.get('Content-Disposition'); - const filename = contentDisposition - ? contentDisposition.split('filename=')[1]?.replace(/"/g, '') - : 'download'; + const extractedFilename = contentDisposition?.split('filename=')[1]?.replace(/"/g, ''); + const filename: string = extractedFilename || 'download'; const downloadLink = document.createElement('a'); downloadLink.href = url; @@ -388,7 +386,7 @@ export async function rejectLevel(requestId: string, levelId: string, rejectionR 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 const payload: any = { title: workflowData.title, diff --git a/src/utils/socket.ts b/src/utils/socket.ts index 62b7399..097d012 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -2,12 +2,36 @@ import { io, Socket } from 'socket.io-client'; 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; - console.log('[Socket] Connecting to:', baseUrl); + console.log('[Socket] Connecting to:', url); - socket = io(baseUrl, { + socket = io(url, { withCredentials: true, transports: ['websocket', 'polling'], path: '/socket.io', diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts index 910b4f7..00c1e82 100644 --- a/src/utils/tokenManager.ts +++ b/src/utils/tokenManager.ts @@ -30,6 +30,7 @@ export const cookieUtils = { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { let cookie = cookies[i]; + if (!cookie) continue; while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length); if (cookie.indexOf(nameEQ) === 0) { 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; 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 now = Date.now(); const buffer = bufferMinutes * 60 * 1000; @@ -415,7 +418,9 @@ export function getTokenExpiration(token: string | null): Date | null { if (!token) return null; 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); } catch { return null; diff --git a/vite.config.ts b/vite.config.ts index a47599d..4044b31 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,9 +2,67 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; 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/ export default defineConfig({ - plugins: [react()], + plugins: [react(), suppressCssWarnings(), ensureChunkOrder()], resolve: { alias: { '@': path.resolve(__dirname, './src'), @@ -23,28 +81,117 @@ export default defineConfig({ build: { outDir: 'dist', 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: { + 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: { - manualChunks: { - 'react-vendor': ['react', 'react-dom'], - 'ui-vendor': ['lucide-react', 'sonner'], - 'radix-vendor': [ - '@radix-ui/react-accordion', - '@radix-ui/react-avatar', - '@radix-ui/react-dialog', - '@radix-ui/react-dropdown-menu', - '@radix-ui/react-label', - '@radix-ui/react-popover', - '@radix-ui/react-select', - '@radix-ui/react-tabs', - ], + // Preserve module structure to prevent circular dependency issues + preserveModules: false, + // Ensure proper chunk ordering and prevent circular dependency issues + chunkFileNames: 'assets/[name]-[hash].js', + // Explicitly define chunk order - React must load before Radix UI + manualChunks(id) { + // CRITICAL FIX: Keep React in main bundle OR ensure it loads first + // The "Cannot access 'React' before initialization" error occurs when + // Radix UI components try to access React before it's initialized + // Option 1: Don't split React - keep it in main bundle (most reliable) + // Option 2: Keep React in separate chunk but ensure it loads first + + // 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: 1000, + chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks }, 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, + }, }, });