setup instruction added
This commit is contained in:
parent
54462b1658
commit
e193b60083
5
.env.example
Normal file
5
.env.example
Normal 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}}
|
||||||
@ -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).
|
|
||||||
@ -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
228
README.md
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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!
|
|
||||||
|
|
||||||
@ -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
28
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
60
setup-env.sh
60
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)
|
# 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"
|
||||||
|
|||||||
51
src/App.tsx
51
src/App.tsx
@ -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')}`;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface LoaderProps {
|
interface LoaderProps {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,10 +138,12 @@ 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) {
|
||||||
mentions.push(match[1]);
|
if (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>
|
||||||
|
|||||||
@ -30,8 +30,6 @@ export function AddApproverModal({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
requestIdDisplay,
|
|
||||||
requestTitle,
|
|
||||||
existingParticipants = [],
|
existingParticipants = [],
|
||||||
currentLevels = []
|
currentLevels = []
|
||||||
}: AddApproverModalProps) {
|
}: AddApproverModalProps) {
|
||||||
|
|||||||
@ -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('');
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 ${
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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')
|
||||||
await uploadDocument(file, requestId, 'SUPPORTING');
|
if (file) {
|
||||||
|
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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
<span>Setting up your session</span>
|
<div className="w-2 h-2 rounded-full transition-all duration-500 bg-green-500"></div>
|
||||||
</div>
|
<span>Setting up your session</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
179
vite.config.ts
179
vite.config.ts
@ -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: 1000,
|
chunkSizeWarningLimit: 1500, // Increased limit since we have manual chunks
|
||||||
},
|
},
|
||||||
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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user