reported bugs fixed

This commit is contained in:
laxmanhalaki 2025-11-10 16:13:38 +05:30
parent 63738c529b
commit 61ba649ac4
24 changed files with 834 additions and 2533 deletions

View File

@ -1,248 +0,0 @@
# Claim Management System - Complete Data Flow
## 🎯 Overview
The claim management system now has complete integration with automatic dealer lookup and proper workflow management.
## 📊 Data Flow Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ 1. User Creates Claim │
│ (ClaimManagementWizard) │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 2. Select Dealer Code (e.g., RE-MH-001) │
│ │
│ Triggers: getDealerInfo(dealerCode) │
│ Returns from dealerDatabase.ts: │
│ • Dealer Name: "Royal Motors Mumbai" │
│ • Email: "dealer@royalmotorsmumbai.com" │
│ • Phone: "+91 98765 12345" │
│ • Address: "Shop No. 12-15, Central Avenue..." │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 3. Complete Form & Submit (formData) │
│ │
│ Captured Fields: │
│ • activityName │
│ • activityType │
│ • activityDate │
│ • location │
│ • dealerCode │
│ • dealerName ┐ │
│ • dealerEmail ├─ Auto-populated from database │
│ • dealerPhone │ │
│ • dealerAddress ┘ │
│ • estimatedBudget │
│ • requestDescription │
│ • periodStart, periodEnd │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 4. App.tsx Creates Request Object │
│ │
│ REQUEST_DATABASE[requestId] = { │
│ id: 'RE-REQ-2024-CM-XXX', │
│ title: '...', │
│ status: 'pending', │
│ currentStep: 1, │
│ totalSteps: 8, │
│ templateType: 'claim-management', │
│ claimDetails: { │
│ activityName: formData.activityName, │
│ dealerEmail: formData.dealerEmail, ← From DB │
│ dealerPhone: formData.dealerPhone, ← From DB │
│ dealerAddress: formData.dealerAddress, ← From DB │
│ estimatedBudget: formData.estimatedBudget, │
│ ...all other fields │
│ }, │
│ approvalFlow: [ 8 steps... ] │
│ } │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 5. MyRequests Shows Request in List │
│ │
│ RE-REQ-2024-CM-001 │
│ Dealer Marketing Activity Claim │
│ Status: Pending | Step 1 of 8 │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 6. User Clicks Request → RequestDetail.tsx │
│ │
│ Fetches from REQUEST_DATABASE[requestId] │
│ Displays all information: │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Overview Tab │ │
│ │ │ │
│ │ 📋 Activity Information │ │
│ │ • Activity Name: "..." │ │
│ │ • Activity Type: "..." │ │
│ │ • Date: "..." │ │
│ │ • Location: "..." │ │
│ │ │ │
│ │ 🏢 Dealer Information │ │
│ │ • Dealer Code: RE-MH-001 │ │
│ │ • Dealer Name: Royal Motors Mumbai │ │
│ │ • Email: dealer@royalmotorsmumbai.com ✓ │ │
│ │ • Phone: +91 98765 12345 ✓ │ │
│ │ • Address: Shop No. 12-15... ✓ │ │
│ │ │ │
│ │ 💰 Claim Request Details │ │
│ │ • Description: "..." │ │
│ │ • Estimated Budget: ₹2,45,000 ✓ │ │
│ │ • Period: Oct 1 - Oct 10 │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 7. Workflow Tab Shows 8-Step Process │
│ │
│ Step 1: Dealer Document Upload [PENDING] │
│ Action: [Upload Proposal Documents] ← Opens modal │
│ │
│ Step 2: Initiator Evaluation [WAITING] │
│ Actions: [Approve] [Request Modifications] │
│ │
│ Step 3: IO Confirmation (Auto) [WAITING] │
│ │
│ Step 4: Department Lead Approval [WAITING] │
│ Action: [Approve & Lock Budget] │
│ │
│ Step 5: Dealer Completion Documents [WAITING] │
│ Action: [Upload Completion Documents] │
│ │
│ Step 6: Initiator Verification [WAITING] │
│ Action: [Verify & Set Amount] ← Opens modal │
│ │
│ Step 7: E-Invoice Generation (Auto) [WAITING] │
│ │
│ Step 8: Credit Note Issuance [WAITING] │
│ Action: [Issue Credit Note] │
└─────────────────────────────────────────────────────────────────┘
```
## 🔄 Key Features Implemented
### 1. Dealer Database Auto-Population
```typescript
// When dealer selected in wizard
handleDealerChange('RE-MH-001')
getDealerInfo('RE-MH-001')
Returns complete dealer object
Auto-fills: name, email, phone, address
```
### 2. Complete Data Capture
- ✅ All activity details
- ✅ All dealer information (from database)
- ✅ Estimated budget
- ✅ Request description
- ✅ Period dates
### 3. Step-Specific Actions
Each workflow step shows relevant action buttons:
- **Upload buttons** for document steps
- **Approve/Reject buttons** for approval steps
- **Set Amount button** for verification step
- **Automatic processing** for system steps
### 4. Modal Integration
- **DealerDocumentModal**: For steps 1 & 5
- Upload multiple documents
- Add dealer comments
- Validation before submit
- **InitiatorVerificationModal**: For step 6
- Review completion documents
- Set final approved amount
- Add verification comments
## 🎨 UI Components
### ClaimManagementWizard
```
Step 1: Claim Details
├── Activity Name & Type
├── Dealer Selection → Auto-fills email, phone, address
├── Date & Location
├── Estimated Budget (new!)
└── Request Description
Step 2: Review & Submit
├── Activity Information Card
├── Dealer Information Card (with email, phone, address)
├── Date & Location Card (with budget)
└── Request Details Card
```
### RequestDetail Overview Tab
```
├── Activity Information
│ ├── Activity Name
│ ├── Activity Type
│ ├── Date
│ └── Location
├── Dealer Information
│ ├── Dealer Code
│ ├── Dealer Name
│ ├── Email ← From dealer database
│ ├── Phone ← From dealer database
│ └── Address ← From dealer database
└── Claim Request Details
├── Description
├── Estimated Budget ← User input
└── Period
```
## 📝 Database Schema
### dealerDatabase.ts Structure
```typescript
{
'RE-MH-001': {
code: 'RE-MH-001',
name: 'Royal Motors Mumbai',
email: 'dealer@royalmotorsmumbai.com',
phone: '+91 98765 12345',
address: 'Shop No. 12-15, Central Avenue, Andheri West',
city: 'Mumbai',
state: 'Maharashtra',
region: 'West',
managerName: 'Rahul Deshmukh'
},
// ... 9 more dealers
}
```
## ✅ Verification Checklist
- [x] Dealer database created with 10+ dealers
- [x] Auto-population works when dealer selected
- [x] All fields captured in claimDetails
- [x] RequestDetail displays all information
- [x] Step-specific action buttons appear
- [x] Modals integrate properly
- [x] 8-step workflow displays correctly
- [x] IDs synchronized across components
- [x] Data flows from wizard → app → detail
## 🚀 Ready for Testing!
The system is now complete and ready for end-to-end testing. All dealer information is automatically fetched from the database, properly saved in the request, and correctly displayed in the detail view.

View File

@ -1,337 +0,0 @@
# Custom Request Details Page Fix
## Problem
Custom requests created through the NewRequestWizard were not displaying in the detail page. Instead, users only saw a "Go Back" button.
## Root Cause
1. **Database Lookup Issue**: Dynamic requests created through wizards were only stored in `App.tsx` component state (`dynamicRequests`), not in the static `CUSTOM_REQUEST_DATABASE`
2. **Component Props**: `RequestDetail` and `ClaimManagementDetail` components weren't receiving the `dynamicRequests` prop
3. **Data Flow Gap**: No connection between newly created requests and the detail view components
## Solution Implemented
### 1. Enhanced Request Creation (`App.tsx`)
Updated `handleNewRequestSubmit` to properly create custom request objects:
```typescript
const newCustomRequest = {
id: requestId,
title: requestData.title,
description: requestData.description,
category: requestData.category,
subcategory: requestData.subcategory,
status: 'pending',
priority: requestData.priority,
amount: requestData.budget,
template: 'custom',
initiator: { ... },
approvalFlow: [...], // Maps approvers from wizard
spectators: [...], // Maps spectators from wizard
documents: [],
auditTrail: [...],
// ... complete request object
};
setDynamicRequests([...dynamicRequests, newCustomRequest]);
```
**Features**:
- Generates unique request ID: `RE-REQ-2024-XXX`
- Maps wizard data to proper request structure
- Creates approval flow from selected approvers
- Adds spectators from wizard
- Initializes audit trail
- Sets proper SLA dates
- Navigates to My Requests page after creation
### 2. Updated Component Props
#### RequestDetail.tsx
```typescript
interface RequestDetailProps {
requestId: string;
onBack?: () => void;
onOpenModal?: (modal: string) => void;
dynamicRequests?: any[]; // NEW
}
```
#### ClaimManagementDetail.tsx
```typescript
interface ClaimManagementDetailProps {
requestId: string;
onBack?: () => void;
onOpenModal?: (modal: string) => void;
dynamicRequests?: any[]; // NEW
}
```
### 3. Enhanced Request Lookup Logic
Both detail components now check both static databases AND dynamic requests:
```typescript
const request = useMemo(() => {
// First check static database
const staticRequest = CUSTOM_REQUEST_DATABASE[requestId];
if (staticRequest) return staticRequest;
// Then check dynamic requests
const dynamicRequest = dynamicRequests.find((req: any) => req.id === requestId);
if (dynamicRequest) return dynamicRequest;
return null;
}, [requestId, dynamicRequests]);
```
### 4. Intelligent Routing (`App.tsx`)
Updated `renderCurrentPage` for `request-detail` case:
```typescript
case 'request-detail':
// Check static databases
const isClaimRequest = CLAIM_MANAGEMENT_DATABASE[selectedRequestId];
const isCustomRequest = CUSTOM_REQUEST_DATABASE[selectedRequestId];
// Check dynamic requests
const dynamicRequest = dynamicRequests.find(...);
const isDynamicClaim = dynamicRequest?.templateType === 'claim-management';
const isDynamicCustom = dynamicRequest && !isDynamicClaim;
// Route to appropriate component with dynamicRequests prop
if (isClaimRequest || isDynamicClaim) {
return <ClaimManagementDetail {...} dynamicRequests={dynamicRequests} />;
} else {
return <RequestDetail {...} dynamicRequests={dynamicRequests} />;
}
```
### 5. Updated handleViewRequest
```typescript
const handleViewRequest = (requestId: string, requestTitle?: string) => {
setSelectedRequestId(requestId);
// Check all sources
const isClaimRequest = CLAIM_MANAGEMENT_DATABASE[requestId];
const isCustomRequest = CUSTOM_REQUEST_DATABASE[requestId];
const dynamicRequest = dynamicRequests.find(...);
const request = isClaimRequest || isCustomRequest || dynamicRequest;
setSelectedRequestTitle(requestTitle || request?.title || 'Unknown Request');
setCurrentPage('request-detail');
};
```
## Data Flow
### Creating a Custom Request
1. User clicks "Raise New Request" → "Custom Request"
2. Fills out NewRequestWizard form:
- Title
- Description
- Category/Subcategory
- Budget
- Priority
- Approvers (multiple)
- Spectators (multiple)
- Tagged Participants
3. On Submit:
- `handleNewRequestSubmit` creates complete request object
- Adds to `dynamicRequests` state
- Navigates to My Requests page
- Shows success toast
4. Viewing the Request:
- User clicks on request in My Requests
- `handleViewRequest` finds request in dynamicRequests
- Routes to `request-detail` page
- `RequestDetail` component receives `dynamicRequests` prop
- Component finds request in dynamicRequests array
- Displays complete request details
## Custom Request Details Page Features
### Header
- Back button
- Request ID with file icon
- Priority badge (urgent/standard)
- Status badge (pending/in-review/approved/rejected)
- Refresh button
- Title display
### SLA Progress Bar
- Color-coded (green/orange/red based on progress)
- Time remaining
- Progress percentage
- Due date
### Tabs
1. **Overview**
- Request Initiator (with avatar, role, email, phone)
- Request Details (description, category, subcategory, amount, dates)
- Quick Actions sidebar
- Spectators list
2. **Workflow**
- Step-by-step approval flow
- Color-coded status indicators
- TAT and elapsed time
- Comments from approvers
3. **Documents**
- List of uploaded documents
- Upload new document button
- View and download actions
4. **Activity**
- Complete audit trail
- Action icons
- User and timestamp for each action
### Quick Actions (Right Sidebar)
- Add Work Note (dark green button)
- Add Approver
- Add Spectator
- Modify SLA
- Approve Request (green)
- Reject Request (red)
## Testing Checklist
✅ **Request Creation**
- [ ] Create custom request through wizard
- [ ] Verify request appears in My Requests
- [ ] Check request ID is properly generated
- [ ] Verify all wizard data is captured
✅ **Request Detail Display**
- [ ] Click on custom request from My Requests
- [ ] Verify detail page loads (not "Go Back" button)
- [ ] Check all fields are populated correctly
- [ ] Verify initiator information displays
- [ ] Check description and category fields
✅ **Workflow Display**
- [ ] Verify approvers from wizard appear in workflow
- [ ] Check first approver is marked as "pending"
- [ ] Verify other approvers are "waiting"
- [ ] Check TAT hours are set
✅ **Spectators**
- [ ] Verify spectators from wizard appear
- [ ] Check avatar generation works
- [ ] Verify role display
✅ **Audit Trail**
- [ ] Check "Request Created" entry
- [ ] Check "Assigned to Approver" entry
- [ ] Verify timestamps are correct
✅ **Quick Actions**
- [ ] Test all quick action buttons
- [ ] Verify modals/toasts appear
- [ ] Check button styling
✅ **Claim Management Independence**
- [ ] Create claim request through ClaimManagementWizard
- [ ] Verify it routes to ClaimManagementDetail (purple theme)
- [ ] Verify custom requests route to RequestDetail (blue theme)
- [ ] Confirm no cross-contamination
## Sample Custom Request Data Structure
```typescript
{
id: 'RE-REQ-2024-004',
title: 'Marketing Campaign Budget Approval',
description: 'Q4 marketing campaign budget request...',
category: 'Marketing & Campaigns',
subcategory: 'Digital Marketing',
status: 'pending',
priority: 'express',
amount: '₹5,00,000',
slaProgress: 0,
slaRemaining: '5 days',
slaEndDate: 'Oct 20, 2024 5:00 PM',
currentStep: 1,
totalSteps: 3,
template: 'custom',
initiator: {
name: 'Current User',
role: 'Employee',
department: 'Marketing',
email: 'current.user@royalenfield.com',
phone: '+91 98765 43290',
avatar: 'CU'
},
approvalFlow: [
{
step: 1,
approver: 'Rajesh Kumar',
role: 'Marketing Director',
status: 'pending',
tatHours: 48,
elapsedHours: 0,
assignedAt: '2024-10-15T...',
comment: null,
timestamp: null
},
// ... more approvers
],
spectators: [
{
name: 'Finance Team',
role: 'Budget Monitoring',
avatar: 'FT'
}
],
documents: [],
auditTrail: [
{
type: 'created',
action: 'Request Created',
details: 'Custom request "..." created',
user: 'Current User',
timestamp: 'Oct 15, 2024 10:30 AM'
}
],
tags: ['custom-request']
}
```
## Future Enhancements
1. **Persistence**: Add backend API integration to persist dynamic requests
2. **Real-time Updates**: WebSocket for live status updates
3. **Document Upload**: Implement actual file upload functionality
4. **Notifications**: Email/push notifications for approvers
5. **Search**: Add search functionality in My Requests
6. **Filters**: Advanced filtering by status, priority, date
7. **Export**: Export request details to PDF
8. **Comments**: Thread-based commenting system
9. **Attachments**: Support for multiple file types
10. **Permissions**: Role-based access control
## Known Limitations
1. Dynamic requests are in-memory only (lost on refresh)
2. No actual file upload (UI only)
3. No real approval actions (mocked)
4. No email notifications
5. No database persistence
## Next Steps
1. Implement backend API for request persistence
2. Add authentication and authorization
3. Implement real approval workflows
4. Add document upload functionality
5. Create notification system
6. Add reporting and analytics
7. Mobile responsive improvements
8. Accessibility enhancements

View File

@ -1,188 +0,0 @@
# Error Fix: "Objects are not valid as a React child"
## Problem
When creating a custom request through the NewRequestWizard, the application threw an error:
```
Error: Objects are not valid as a React child (found: object with keys {email, name, level, tat, tatType})
```
## Root Cause
The NewRequestWizard stores approvers, spectators, ccList, and invitedUsers as arrays of objects with the structure:
```typescript
{
email: string,
name: string,
level: number,
tat: number,
tatType: 'hours' | 'days'
}
```
When these objects were passed to `handleNewRequestSubmit` in App.tsx, they were being mapped to the request structure, but the mapping wasn't properly extracting the string values from the objects. Instead, entire objects were being assigned to fields that should contain strings.
## Solution
### 1. Enhanced Approver Mapping
Updated the `approvalFlow` mapping in `handleNewRequestSubmit` to properly extract values:
```typescript
approvalFlow: (requestData.approvers || [])
.filter((a: any) => a) // Filter out null/undefined
.map((approver: any, index: number) => {
// Extract name from email if name is not available
const approverName = approver?.name || approver?.email?.split('@')[0] || `Approver ${index + 1}`;
const approverEmail = approver?.email || '';
return {
step: index + 1,
approver: `${approverName}${approverEmail ? ` (${approverEmail})` : ''}`, // STRING, not object
role: approver?.role || `Level ${approver?.level || index + 1} Approver`,
status: index === 0 ? 'pending' : 'waiting',
tatHours: approver?.tat ? (typeof approver.tat === 'string' ? parseInt(approver.tat) : approver.tat) : 48,
elapsedHours: index === 0 ? 0 : 0,
assignedAt: index === 0 ? new Date().toISOString() : null,
comment: null,
timestamp: null
};
})
```
**Key changes:**
- Added `.filter((a: any) => a)` to remove null/undefined entries
- Properly extract `approverName` from `name` or `email`
- Build a display string combining name and email
- Convert TAT to number (handles both string and number inputs)
### 2. Enhanced Spectator Mapping
Updated spectators mapping to properly extract values:
```typescript
spectators: (requestData.spectators || [])
.filter((s: any) => s && (s.name || s.email)) // Filter invalid entries
.map((spectator: any) => {
const name = spectator?.name || spectator?.email?.split('@')[0] || 'Observer';
return {
name: name, // STRING, not object
role: spectator?.role || spectator?.department || 'Observer',
avatar: name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'OB'
};
})
```
**Key changes:**
- Filter out entries without name or email
- Extract name from email if needed
- Safe avatar generation with fallback
### 3. Added Missing Fields
Added fields required by MyRequests component:
```typescript
currentApprover: requestData.approvers?.[0]?.name || requestData.approvers?.[0]?.email?.split('@')[0] || 'Pending Assignment',
approverLevel: `1 of ${requestData.approvers?.length || 1}`,
submittedDate: new Date().toISOString(),
estimatedCompletion: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
```
### 4. Fixed Audit Trail Message
Updated audit trail to safely extract approver name:
```typescript
details: `Request assigned to ${requestData.approvers?.[0]?.name || requestData.approvers?.[0]?.email || 'first approver'}`
```
## Testing Checklist
✅ **Create Custom Request**
- [ ] Fill out NewRequestWizard with title, description
- [ ] Add 2-3 approvers with emails
- [ ] Add spectators (optional)
- [ ] Submit request
✅ **Verify No Errors**
- [ ] No console errors about "Objects are not valid as React child"
- [ ] Request appears in My Requests
- [ ] Can click on request
✅ **Verify Detail Page**
- [ ] Request detail page loads
- [ ] Approver names display correctly (not [object Object])
- [ ] Workflow tab shows all approvers
- [ ] Spectators display correctly (if added)
✅ **Verify My Requests Display**
- [ ] Request shows in list
- [ ] Current approver displays as string
- [ ] Approver level shows correctly (e.g., "1 of 3")
## Common Patterns to Avoid
### ❌ Bad: Rendering Objects Directly
```typescript
<span>{approver}</span> // If approver is {email: "...", name: "..."}
```
### ✅ Good: Extract String First
```typescript
<span>{approver.name || approver.email}</span>
```
### ❌ Bad: Assigning Object to String Field
```typescript
{
approver: approverObject // {email: "...", name: "..."}
}
```
### ✅ Good: Extract String Value
```typescript
{
approver: approverObject.name || approverObject.email
}
```
## Related Files Modified
1. **App.tsx** - `handleNewRequestSubmit` function
- Enhanced approver mapping
- Enhanced spectator mapping
- Added missing fields for MyRequests compatibility
- Fixed audit trail messages
## Data Flow
```
NewRequestWizard (formData)
├── approvers: [{email, name, level, tat, tatType}, ...]
├── spectators: [{email, name, role, department}, ...]
└── ...other fields
handleNewRequestSubmit (App.tsx)
├── Maps approvers → approvalFlow with STRING values
├── Maps spectators → spectators with STRING values
└── Creates complete request object
dynamicRequests state (App.tsx)
├── Stored in memory
└── Passed to components
RequestDetail / MyRequests
├── Receives proper data structure
└── Renders strings (no object errors)
```
## Prevention Tips
1. **Always validate data types** when mapping from wizard to database
2. **Extract primitive values** from objects before assigning to display fields
3. **Add TypeScript interfaces** to catch type mismatches early
4. **Test with console.log** before rendering to verify data structure
5. **Use optional chaining** (`?.`) to safely access nested properties
## Future Improvements
1. Add TypeScript interfaces for wizard form data
2. Add TypeScript interfaces for request objects
3. Create validation functions for data transformation
4. Add unit tests for data mapping functions
5. Create reusable mapping utilities

View File

@ -1,306 +0,0 @@
# 🔄 Migration Guide - Project Setup Complete
## ✅ What Has Been Created
### Configuration Files ✓
- ✅ `package.json` - Dependencies and scripts
- ✅ `tsconfig.json` - TypeScript configuration
- ✅ `tsconfig.node.json` - Node TypeScript config
- ✅ `vite.config.ts` - Vite build configuration
- ✅ `tailwind.config.ts` - Tailwind CSS configuration
- ✅ `postcss.config.js` - PostCSS configuration
- ✅ `eslint.config.js` - ESLint configuration
- ✅ `.prettierrc` - Prettier configuration
- ✅ `.gitignore` - Git ignore rules
- ✅ `.env.example` - Environment variables template
- ✅ `index.html` - HTML entry point
### VS Code Configuration ✓
- ✅ `.vscode/settings.json` - Editor settings
- ✅ `.vscode/extensions.json` - Recommended extensions
### Documentation ✓
- ✅ `README.md` - Comprehensive project documentation
- ✅ `MIGRATION_GUIDE.md` - This file
### Source Files Created ✓
- ✅ `src/main.tsx` - Application entry point
- ✅ `src/vite-env.d.ts` - Vite environment types
- ✅ `src/types/index.ts` - TypeScript type definitions
---
## 🚀 Next Steps - File Migration
### Step 1: Install Dependencies
\`\`\`bash
npm install
\`\`\`
This will install all required packages (~5 minutes).
### Step 2: Migrate Files to src Directory
You need to manually move the existing files to the `src` directory:
#### A. Move App.tsx
\`\`\`bash
# Windows Command Prompt
move App.tsx src\\App.tsx
# Or manually drag and drop in VS Code
\`\`\`
#### B. Move Components Directory
\`\`\`bash
# Windows Command Prompt
move components src\\components
# Or manually drag and drop in VS Code
\`\`\`
#### C. Move Utils Directory
\`\`\`bash
# Windows Command Prompt
move utils src\\utils
# Or manually drag and drop in VS Code
\`\`\`
#### D. Move Styles Directory
\`\`\`bash
# Windows Command Prompt
move styles src\\styles
# Or manually drag and drop in VS Code
\`\`\`
### Step 3: Update Import Paths
After moving files, you'll need to update import statements to use path aliases.
#### Example Changes:
**Before:**
\`\`\`typescript
import { Layout } from './components/Layout';
import { Dashboard } from './components/Dashboard';
\`\`\`
**After:**
\`\`\`typescript
import { Layout } from '@/components/Layout';
import { Dashboard } from '@/components/Dashboard';
\`\`\`
**Files that need updating:**
1. `src/App.tsx` - Update all component imports
2. All files in `src/components/` - Update relative imports
3. All modal files in `src/components/modals/`
### Step 4: Fix Sonner Import
In `src/App.tsx`, update the sonner import:
**Before:**
\`\`\`typescript
import { toast } from 'sonner@2.0.3';
\`\`\`
**After:**
\`\`\`typescript
import { toast } from 'sonner';
\`\`\`
### Step 5: Start Development Server
\`\`\`bash
npm run dev
\`\`\`
The app should open at `http://localhost:3000`
---
## 🔍 Common Issues & Solutions
### Issue 1: Module not found errors
**Problem:** TypeScript can't find modules after migration.
**Solution:**
1. Restart VS Code TypeScript server: `Ctrl+Shift+P` → "TypeScript: Restart TS Server"
2. Clear node_modules and reinstall:
\`\`\`bash
rm -rf node_modules package-lock.json
npm install
\`\`\`
### Issue 2: Path alias not working
**Problem:** `@/` imports show errors.
**Solution:**
1. Check `tsconfig.json` paths configuration
2. Check `vite.config.ts` resolve.alias configuration
3. Restart VS Code
### Issue 3: Tailwind classes not applying
**Problem:** Styles not working after migration.
**Solution:**
1. Ensure `globals.css` is imported in `src/main.tsx`
2. Check `tailwind.config.ts` content paths
3. Restart dev server: `Ctrl+C` then `npm run dev`
### Issue 4: Build errors
**Problem:** TypeScript compilation errors.
**Solution:**
1. Run type check: `npm run type-check`
2. Fix any TypeScript errors shown
3. Run build again: `npm run build`
---
## 📋 Migration Checklist
Use this checklist to track your migration progress:
### Files Migration
- [ ] Installed dependencies (`npm install`)
- [ ] Moved `App.tsx` to `src/`
- [ ] Moved `components/` to `src/components/`
- [ ] Moved `utils/` to `src/utils/`
- [ ] Moved `styles/` to `src/styles/`
- [ ] Created `src/main.tsx` (already done)
### Import Updates
- [ ] Updated imports in `src/App.tsx`
- [ ] Updated imports in `src/components/Layout.tsx`
- [ ] Updated imports in `src/components/Dashboard.tsx`
- [ ] Updated imports in all other component files
- [ ] Fixed `sonner` import in `App.tsx`
### Testing
- [ ] Dev server starts successfully (`npm run dev`)
- [ ] Application loads at `http://localhost:3000`
- [ ] No console errors
- [ ] Dashboard displays correctly
- [ ] Navigation works
- [ ] New request wizard works
- [ ] Claim management wizard works
### Code Quality
- [ ] ESLint passes (`npm run lint`)
- [ ] TypeScript compiles (`npm run type-check`)
- [ ] Code formatted (`npm run format`)
- [ ] Build succeeds (`npm run build`)
### Environment
- [ ] Created `.env` from `.env.example`
- [ ] Updated environment variables if needed
- [ ] VS Code extensions installed
---
## 🎯 After Migration
### 1. Clean Up Old Files
After confirming everything works in `src/`:
\`\`\`bash
# Delete old documentation files from root (optional)
# Keep only if you want them at root level
\`\`\`
### 2. Commit Changes
\`\`\`bash
git add .
git commit -m "feat: migrate to standard React project structure with Vite"
\`\`\`
### 3. Update Team
Inform team members about:
- New project structure
- Updated npm scripts
- Path alias usage (`@/`)
- Required VS Code extensions
---
## 📚 Additional Resources
### Documentation
- [Vite Documentation](https://vitejs.dev/)
- [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/)
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
- [shadcn/ui Components](https://ui.shadcn.com/)
### Scripts Reference
\`\`\`bash
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Check for linting errors
npm run lint:fix # Auto-fix linting errors
npm run format # Format code with Prettier
npm run type-check # Check TypeScript types
\`\`\`
---
## 💡 Tips for Development
### 1. Use Path Aliases
\`\`\`typescript
// Good ✓
import { Button } from '@/components/ui/button';
import { getDealerInfo } from '@/utils/dealerDatabase';
// Avoid ✗
import { Button } from '../../../components/ui/button';
\`\`\`
### 2. Type Safety
\`\`\`typescript
// Import types
import type { Request, DealerInfo } from '@/types';
// Use them in your components
const request: Request = { ... };
\`\`\`
### 3. Code Formatting
Set up auto-format on save in VS Code (already configured in `.vscode/settings.json`)
### 4. Commit Conventions
Use conventional commits:
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation
- `style:` for formatting changes
- `refactor:` for code refactoring
---
## ❓ Need Help?
If you encounter issues:
1. Check this migration guide
2. Check the main README.md
3. Review error messages carefully
4. Check VS Code Problems panel
5. Restart VS Code TypeScript server
6. Clear node_modules and reinstall
---
**Migration prepared by the Development Team**
**Date: 2024**

View File

@ -1,217 +0,0 @@
# Request Type Separation Implementation
## Overview
Successfully implemented complete separation between **Custom Requests** and **Claim Management Requests** to ensure independent processes, databases, and components.
## Architecture
### 1. Separate Databases
#### Custom Request Database
- **Location**: `/utils/customRequestDatabase.ts`
- **Purpose**: Stores all custom requests created via NewRequestWizard
- **Export**: `CUSTOM_REQUEST_DATABASE`
- **API Endpoints**: `CUSTOM_REQUEST_API_ENDPOINTS`
- **Features**:
- User-defined workflow
- Custom approvers added during creation
- Spectators and tagged participants
- Category/subcategory fields
- Flexible approval steps
#### Claim Management Database
- **Location**: `/utils/claimManagementDatabase.ts`
- **Purpose**: Stores all claim management requests created via ClaimManagementWizard
- **Export**: `CLAIM_MANAGEMENT_DATABASE`
- **API Endpoints**: `CLAIM_MANAGEMENT_API_ENDPOINTS`
- **Features**:
- Fixed 8-step workflow process
- Dealer information (code, name, contact, address)
- Activity details (name, type, location, date)
- Budget tracking
- Specialized modals (DealerDocumentModal, InitiatorVerificationModal)
### 2. Separate Detail Components
#### Request Detail Component
- **Location**: `/components/RequestDetail.tsx`
- **Purpose**: Display custom/standard requests only
- **Database**: Uses `CUSTOM_REQUEST_DATABASE`
- **Features**:
- Standard initiator information
- Category and subcategory display
- Approvers shown from request creation
- General description and specifications
- Flexible workflow steps
- Standard action buttons
#### Claim Management Detail Component
- **Location**: `/components/ClaimManagementDetail.tsx`
- **Purpose**: Display claim management requests only
- **Database**: Uses `CLAIM_MANAGEMENT_DATABASE`
- **Features**:
- Dealer information prominently displayed
- Activity information section
- 8-step workflow with specific actions per step
- Budget/amount tracking
- Claim-specific modals (dealer docs, verification)
- Purple theme for claim management branding
- Step-specific action buttons (Upload Documents, Verify Amount, etc.)
### 3. App.tsx Routing Logic
The main App component now includes intelligent routing:
```typescript
case 'request-detail':
const isClaimRequest = CLAIM_MANAGEMENT_DATABASE[selectedRequestId];
const isCustomRequest = CUSTOM_REQUEST_DATABASE[selectedRequestId];
if (isClaimRequest) {
return <ClaimManagementDetail ... />;
} else if (isCustomRequest) {
return <RequestDetail ... />;
}
```
### 4. API Endpoint Separation
Each database file includes its own API endpoint constants:
#### Custom Request Endpoints
- `/api/v1/custom-request/*`
- Includes: create, update, get, list, approve, reject, add approver/spectator/tagged, documents, work notes, etc.
#### Claim Management Endpoints
- `/api/v1/claim-management/*`
- Includes: create, update, get, list, dealer document upload, initiator evaluate, generate IO, department approval, completion docs, verify, e-invoice, credit note, etc.
## Benefits
### 1. Complete Independence
- Changes to claim management don't affect custom requests
- Changes to custom requests don't affect claim management
- Each process has its own data structure and business logic
### 2. Future-Proof
- Easy to add new template types (e.g., Budget Approval, Travel Requests)
- Each new template gets its own database and detail component
- No cross-contamination between processes
### 3. API Ready
- Separate endpoints allow different backend services
- Different authentication/authorization per process type
- Different data validation and business rules
### 4. Maintainability
- Clear separation of concerns
- Easy to debug issues (know which system to check)
- Independent testing for each process type
## Data Flow
### Custom Request Flow
1. User clicks "Custom Request" in NewRequestWizard
2. Fills out form with custom approvers, spectators, etc.
3. Submitted to `CUSTOM_REQUEST_DATABASE`
4. Clicking on request triggers `RequestDetail` component
5. All actions use `CUSTOM_REQUEST_API_ENDPOINTS`
### Claim Management Flow
1. User clicks "Existing Template" → "Claim Management"
2. Navigates to ClaimManagementWizard
3. Fills out claim-specific form (dealer, activity, budget)
4. Submitted to `CLAIM_MANAGEMENT_DATABASE`
5. Clicking on claim triggers `ClaimManagementDetail` component
6. All actions use `CLAIM_MANAGEMENT_API_ENDPOINTS`
## Database Schema Differences
### Custom Request
```typescript
{
id: string
title: string
description: string
category: string
subcategory: string
status: string
priority: string
template: 'custom'
approvalFlow: [...] // User-defined
// No claimDetails
}
```
### Claim Management Request
```typescript
{
id: string
title: string
template: 'claim-management'
claimDetails: {
activityName: string
activityType: string
location: string
dealerCode: string
dealerName: string
dealerEmail: string
dealerPhone: string
dealerAddress: string
estimatedBudget: string
// ... more claim-specific fields
}
approvalFlow: [...] // Fixed 8-step process
}
```
## Visual Differences
### Request Detail (Custom)
- Blue theme
- Standard file icon
- Category/Subcategory displayed
- "Request Detail" terminology
- Generic approval buttons
### Claim Management Detail (Purple)
- Purple theme throughout
- Receipt/claim icon
- "Claim Management" badge
- Dealer and Activity sections
- "Claim Amount" instead of "Total Amount"
- Step-specific buttons (Upload Documents, Verify Amount, etc.)
- "Claim Activity Timeline" instead of "Activity Timeline"
## Migration Notes
### Legacy Database
- Old `REQUEST_DATABASE` in App.tsx is now marked as `LEGACY_REQUEST_DATABASE`
- Combined export `REQUEST_DATABASE` = custom + claim for backward compatibility
- Will be removed in future once all components updated
### Components to Update (Future)
- Dashboard.tsx - update to use both databases
- RequestsList.tsx - update to use both databases
- MyRequests.tsx - update to use both databases
## Testing Checklist
- [ ] Custom request creation works
- [ ] Custom request detail page displays correctly
- [ ] Custom request actions (approve/reject) work
- [ ] Claim management creation works
- [ ] Claim management detail page displays correctly
- [ ] Claim-specific actions work (dealer upload, verification)
- [ ] No cross-contamination between types
- [ ] Routing correctly identifies request type
- [ ] Proper error handling for missing requests
## Future Enhancements
1. Add more template types (Budget Approval, Travel Request, etc.)
2. Create utility functions for database operations
3. Add TypeScript interfaces for better type safety
4. Implement actual API integration
5. Add request type indicators in lists
6. Create admin panel for template management

View File

@ -1,283 +0,0 @@
# 🎯 Quick Setup Instructions
## ✅ Project Setup Complete!
Your Royal Enfield Approval Portal has been configured with industry-standard React development tools and structure.
---
## 🚀 Quick Start (5 Minutes)
### Option 1: Automated Migration (Recommended)
**For Windows PowerShell:**
```powershell
# 1. Run the migration script
.\migrate-files.ps1
# 2. Install dependencies
npm install
# 3. Start development server
npm run dev
```
### Option 2: Manual Migration
**If you prefer manual control:**
```bash
# 1. Create src directories
mkdir src\components src\utils src\styles
# 2. Move files
move App.tsx src\
move components src\
move utils src\
move styles src\
# 3. Install dependencies
npm install
# 4. Start development server
npm run dev
```
---
## 📦 What Was Created
### Core Configuration ✅
- ✅ `package.json` - 50+ dependencies installed
- ✅ `vite.config.ts` - Build tool (fast, modern)
- ✅ `tsconfig.json` - TypeScript settings
- ✅ `tailwind.config.ts` - Styling configuration
- ✅ `eslint.config.js` - Code quality rules
- ✅ `.prettierrc` - Code formatting
### Project Structure ✅
```
Re_Figma_Code/
├── src/ ← NEW! All code goes here
│ ├── main.tsx ← Entry point (created)
│ ├── App.tsx ← Move here
│ ├── components/ ← Move here
│ ├── utils/ ← Move here
│ ├── styles/ ← Move here
│ └── types/ ← Type definitions (created)
├── public/ ← Static assets
├── index.html ← HTML entry
└── [config files] ← All created
```
### VS Code Setup ✅
- ✅ `.vscode/settings.json` - Auto-format, Tailwind IntelliSense
- ✅ `.vscode/extensions.json` - Recommended extensions
---
## 🔧 After Running Setup
### 1. Fix Imports in App.tsx
**Update these imports:**
```typescript
// OLD (relative paths)
import { Layout } from './components/Layout';
import { Dashboard } from './components/Dashboard';
import { toast } from 'sonner@2.0.3';
// NEW (path aliases)
import { Layout } from '@/components/Layout';
import { Dashboard } from '@/components/Dashboard';
import { toast } from 'sonner';
```
### 2. Update Component Imports
**In all component files, change:**
```typescript
// OLD
import { Button } from './ui/button';
import { Card } from '../ui/card';
// NEW
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
```
### 3. Verify Everything Works
```bash
# Check for TypeScript errors
npm run type-check
# Check for linting issues
npm run lint
# Format code
npm run format
# Build for production (test)
npm run build
```
---
## 🎨 Development Workflow
### Daily Development
```bash
npm run dev # Start dev server (http://localhost:3000)
```
### Code Quality
```bash
npm run lint # Check for issues
npm run lint:fix # Auto-fix issues
npm run format # Format code with Prettier
```
### Building
```bash
npm run build # Production build
npm run preview # Preview production build
```
---
## 🆘 Troubleshooting
### Issue: "Module not found"
**Solution:**
```bash
# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install
```
### Issue: "@/ path alias not working"
**Solution:**
1. Restart VS Code
2. Press `Ctrl+Shift+P` → "TypeScript: Restart TS Server"
### Issue: "Tailwind classes not applying"
**Solution:**
1. Check `src/main.tsx` imports `'./styles/globals.css'`
2. Restart dev server: `Ctrl+C` then `npm run dev`
### Issue: Build errors
**Solution:**
```bash
npm run type-check # See TypeScript errors
npm run lint # See ESLint errors
```
---
## 📚 Important Files
### Environment Variables
Copy and edit `.env.example`:
```bash
cp .env.example .env
```
Edit values:
```env
VITE_API_BASE_URL=http://localhost:5000/api
VITE_APP_NAME=Royal Enfield Approval Portal
```
### Type Definitions
Use types from `src/types/index.ts`:
```typescript
import type { Request, DealerInfo, Priority } from '@/types';
const request: Request = { /* ... */ };
```
---
## ✨ New Features Available
### 1. Path Aliases
```typescript
import { Button } from '@/components/ui/button';
import { getDealerInfo } from '@/utils/dealerDatabase';
import type { Request } from '@/types';
```
### 2. Code Quality Tools
- **ESLint** - Catches bugs and enforces best practices
- **Prettier** - Consistent code formatting
- **TypeScript** - Type safety and IntelliSense
### 3. Optimized Build
- **Code splitting** - Faster load times
- **Tree shaking** - Smaller bundle size
- **Source maps** - Easy debugging
### 4. Development Experience
- **Hot Module Replacement** - Instant updates
- **Fast Refresh** - Preserve component state
- **Better Error Messages** - Easier debugging
---
## 🎯 Next Steps
1. ✅ Run migration script or move files manually
2. ✅ Install dependencies: `npm install`
3. ✅ Update imports to use `@/` aliases
4. ✅ Fix sonner import
5. ✅ Start dev server: `npm run dev`
6. ✅ Test all features work
7. ✅ Commit changes to git
---
## 📖 Documentation
- **README.md** - Comprehensive project documentation
- **MIGRATION_GUIDE.md** - Detailed migration steps
- **package.json** - All available scripts
---
## 🤝 Team Guidelines
### Code Style
- Use path aliases (`@/`) for all imports
- Format code before committing (`npm run format`)
- Fix linting issues (`npm run lint:fix`)
- Write TypeScript types (avoid `any`)
### Git Commits
```bash
git add .
git commit -m "feat: add new feature"
git commit -m "fix: resolve bug"
git commit -m "docs: update documentation"
```
---
## ✅ Success Checklist
- [ ] Dependencies installed (`npm install`)
- [ ] Files migrated to `src/`
- [ ] Imports updated to use `@/`
- [ ] Sonner import fixed
- [ ] Dev server runs (`npm run dev`)
- [ ] No console errors
- [ ] Application loads correctly
- [ ] All features work
- [ ] Build succeeds (`npm run build`)
---
**🎉 You're all set! Happy coding!**
For detailed help, see `MIGRATION_GUIDE.md` or `README.md`

View File

@ -1,183 +0,0 @@
# SLA Tracking with Working Hours - Implementation Guide
## Overview
The SLA tracking system automatically **pauses during non-working hours** and **resumes during working hours**, ensuring accurate TAT (Turnaround Time) calculations.
## 🎯 Features
**Automatic Pause/Resume** - Stops counting during:
- Weekends (Saturday & Sunday)
- Non-working hours (before 9 AM, after 6 PM)
- Holidays (from database)
**Real-Time Updates** - Progress updates every minute
**Visual Indicators** - Shows when paused vs active
**Working Hours Display** - Shows elapsed and remaining in working hours (e.g., "2d 3h")
**Next Resume Time** - Shows when tracking will resume during paused state
## 📁 Components Created
### 1. **Utility: `slaTracker.ts`**
Core calculation functions:
- `isWorkingTime()` - Check if current time is working hours
- `calculateElapsedWorkingHours()` - Count only working hours
- `calculateRemainingWorkingHours()` - Working hours until deadline
- `getSLAStatus()` - Complete SLA status with pause/resume info
- `formatWorkingHours()` - Format hours as "2d 3h"
### 2. **Hook: `useSLATracking.ts`**
React hook for real-time tracking:
```typescript
const slaStatus = useSLATracking(startDate, deadline);
// Returns: { progress, elapsedHours, remainingHours, isPaused, statusText, ... }
```
### 3. **Component: `SLATracker.tsx`**
Visual component with pause/resume indicators:
```tsx
<SLATracker
startDate={request.createdAt}
deadline={request.dueDate}
showDetails={true}
/>
```
## 🚀 Usage Examples
### In MyRequests Page (Already Integrated)
```tsx
{request.createdAt && request.dueDate &&
request.status !== 'approved' && request.status !== 'rejected' && (
<SLATracker
startDate={request.createdAt}
deadline={request.dueDate}
showDetails={true}
/>
)}
```
### In RequestDetail Page
Replace the existing SLA progress bar with:
```tsx
import { SLATracker } from '@/components/sla/SLATracker';
// In the SLA Progress section:
<SLATracker
startDate={request.createdAt}
deadline={request.slaEndDate}
showDetails={true}
className="mt-2"
/>
```
### In OpenRequests Page
```tsx
{request.createdAt && request.dueDate && (
<SLATracker
startDate={request.createdAt}
deadline={request.dueDate}
showDetails={false} // Compact view
/>
)}
```
## 🎨 Visual States
### Active (During Working Hours)
```
SLA Progress [▶️ Active] [On track]
████████░░░░░░░░ 45.2%
Elapsed: 1d 4h Remaining: 1d 4h
```
### Paused (Outside Working Hours)
```
SLA Progress [⏸️ Paused] [On track]
████████⏸️░░░░░░ 45.2%
Elapsed: 1d 4h Remaining: 1d 4h
⚠️ Resumes in 14h 30m
```
### Critical (>75%)
```
SLA Progress [▶️ Active] [SLA critical]
█████████████████████░ 87.5%
Elapsed: 6d 6h Remaining: 1d 2h
```
### Breached (100%)
```
SLA Progress [⏸️ Paused] [SLA breached]
███████████████████████ 100.0%
Elapsed: 8d 0h Remaining: 0h
⚠️ Resumes in 2h 15m
```
## ⚙️ Configuration
Working hours are defined in `slaTracker.ts`:
```typescript
const WORK_START_HOUR = 9; // 9 AM
const WORK_END_HOUR = 18; // 6 PM
const WORK_START_DAY = 1; // Monday
const WORK_END_DAY = 5; // Friday
```
To change these, update the constants in the utility file.
## 🔄 How It Works
### Backend (Already Implemented)
1. ✅ Calculates deadlines using `addWorkingHours()` (skips weekends, holidays, non-work hours)
2. ✅ Stores calculated deadline in database
3. ✅ TAT scheduler triggers notifications at 50%, 75%, 100% (accounting for working hours)
### Frontend (New Implementation)
1. **Receives** pre-calculated deadline from backend
2. **Calculates** real-time elapsed working hours from start to now
3. **Displays** accurate progress that only counts working time
4. **Shows** pause indicator when outside working hours
5. **Updates** every minute automatically
### Example Flow:
**Friday 4:00 PM** (within working hours)
- SLA Progress: [▶️ Active] 25%
- Shows real-time progress
**Friday 6:01 PM** (after hours)
- SLA Progress: [⏸️ Paused] 25%
- Shows "Resumes in 15h" (Monday 9 AM)
**Monday 9:00 AM** (work resumes)
- SLA Progress: [▶️ Active] 25%
- Continues from where it left off
**Monday 10:00 AM** (1 working hour later)
- SLA Progress: [▶️ Active] 30%
- Progress updates only during working hours
## 🎯 Benefits
1. **Accurate SLA Tracking** - Only counts actual working time
2. **User Transparency** - Users see when SLA is paused
3. **Realistic Deadlines** - No false urgency during weekends
4. **Aligned with Backend** - Frontend display matches backend calculations
5. **Real-Time Updates** - Live progress without page refresh
## 🧪 Testing
Test different scenarios:
1. **During working hours** → Should show "Active" badge
2. **After 6 PM** → Should show "Paused" and resume time
3. **Weekends** → Should show "Paused" and Monday 9 AM resume
4. **Progress calculation** → Should only count working hours
## 📝 Notes
- The frontend assumes the backend has already calculated the correct deadline
- Progress bars update every 60 seconds
- Paused state is visual only - actual TAT calculations are on backend
- For holidays, consider integrating with backend holiday API in future enhancement

View File

@ -1,342 +0,0 @@
# 🚀 START HERE - Royal Enfield Approval Portal
## ✅ Your Project is Now Configured!
All standard React configuration files have been created. Your project now follows industry best practices.
---
## 🎯 Quick Start (Choose One Method)
### Method 1: PowerShell Script (Easiest - Windows)
```powershell
# Run in PowerShell
.\migrate-files.ps1
npm install
npm run dev
```
### Method 2: Manual (All Platforms)
```bash
# 1. Create src structure
mkdir -p src/components src/utils src/styles
# 2. Move files
mv App.tsx src/
mv components src/
mv utils src/
mv styles src/
# 3. Install & run
npm install
npm run dev
```
---
## 📁 What Changed
### ✅ Created Configuration Files
```
✅ package.json - Dependencies & scripts
✅ vite.config.ts - Build configuration
✅ tsconfig.json - TypeScript settings
✅ tailwind.config.ts - Tailwind CSS config
✅ eslint.config.js - Code quality rules
✅ .prettierrc - Code formatting
✅ postcss.config.js - CSS processing
✅ .gitignore - Git ignore rules
✅ index.html - Entry HTML file
```
### ✅ Created Project Structure
```
src/
├── main.tsx ✅ Created - App entry point
├── vite-env.d.ts ✅ Created - Vite types
├── types/
│ └── index.ts ✅ Created - TypeScript types
└── lib/
└── utils.ts ✅ Created - Utility functions
Need to move:
├── App.tsx → src/App.tsx
├── components/ → src/components/
├── utils/ → src/utils/
└── styles/ → src/styles/
```
### ✅ Created Documentation
```
✅ README.md - Full project documentation
✅ MIGRATION_GUIDE.md - Detailed migration steps
✅ SETUP_INSTRUCTIONS.md - Quick setup guide
✅ START_HERE.md - This file
```
### ✅ VS Code Configuration
```
.vscode/
├── settings.json ✅ Auto-format, Tailwind IntelliSense
└── extensions.json ✅ Recommended extensions
```
---
## 🔄 Migration Steps
### Step 1: Move Files (Pick One)
**Option A - PowerShell Script:**
```powershell
.\migrate-files.ps1
```
**Option B - Manual:**
```bash
move App.tsx src\
move components src\
move utils src\
move styles src\
```
### Step 2: Install Dependencies
```bash
npm install
```
This installs ~50 packages (takes 2-3 minutes).
### Step 3: Update Imports in src/App.tsx
**Find and Replace in App.tsx:**
1. Change all component imports:
```typescript
// OLD
import { Layout } from './components/Layout';
import { Dashboard } from './components/Dashboard';
// ... all other component imports
// NEW
import { Layout } from '@/components/Layout';
import { Dashboard } from '@/components/Dashboard';
// ... use @/ for all imports
```
2. Fix sonner import:
```typescript
// OLD
import { toast } from 'sonner@2.0.3';
// NEW
import { toast } from 'sonner';
```
### Step 4: Update Component Files
In all files under `src/components/`, change relative imports:
```typescript
// OLD in any component file
import { Button } from './ui/button';
import { Card } from '../ui/card';
// NEW
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
```
### Step 5: Start Development Server
```bash
npm run dev
```
Visit: http://localhost:3000
---
## 🛠️ Available Commands
```bash
# Development
npm run dev # Start dev server (port 3000)
# Build
npm run build # Build for production
npm run preview # Preview production build
# Code Quality
npm run lint # Check for errors
npm run lint:fix # Auto-fix errors
npm run format # Format with Prettier
npm run type-check # Check TypeScript types
```
---
## ✨ New Features You Get
### 1. **Path Aliases** - Cleaner Imports
```typescript
// Before
import { Button } from '../../../components/ui/button';
// After
import { Button } from '@/components/ui/button';
```
### 2. **TypeScript Types** - Better IntelliSense
```typescript
import type { Request, DealerInfo } from '@/types';
const request: Request = { /* auto-complete works! */ };
```
### 3. **Code Quality** - Auto-fix on Save
- ESLint catches bugs
- Prettier formats code
- TypeScript ensures type safety
### 4. **Fast Development**
- Hot Module Replacement (HMR)
- Instant updates on file save
- Optimized build with code splitting
---
## 🆘 Common Issues & Fixes
### Issue 1: "Cannot find module '@/...'"
**Fix:**
```bash
# Restart TypeScript server
# In VS Code: Ctrl+Shift+P → "TypeScript: Restart TS Server"
```
### Issue 2: "Module not found: sonner@2.0.3"
**Fix in src/App.tsx:**
```typescript
// Change this:
import { toast } from 'sonner@2.0.3';
// To this:
import { toast } from 'sonner';
```
### Issue 3: Tailwind classes not working
**Fix:**
1. Ensure `src/main.tsx` has: `import './styles/globals.css';`
2. Restart dev server: `Ctrl+C` then `npm run dev`
### Issue 4: Build fails
**Fix:**
```bash
npm run type-check # See what TypeScript errors exist
npm run lint # See what ESLint errors exist
```
---
## 📚 Documentation
| File | Purpose |
|------|---------|
| `README.md` | Full project documentation |
| `MIGRATION_GUIDE.md` | Step-by-step migration |
| `SETUP_INSTRUCTIONS.md` | Quick setup guide |
| `START_HERE.md` | This file - quick overview |
---
## ✅ Success Checklist
Track your progress:
- [ ] Run migration script OR move files manually
- [ ] Run `npm install` (2-3 minutes)
- [ ] Update imports in `src/App.tsx` to use `@/`
- [ ] Fix sonner import in `src/App.tsx`
- [ ] Update imports in all component files
- [ ] Run `npm run dev`
- [ ] Open http://localhost:3000
- [ ] Verify app loads without errors
- [ ] Test dashboard navigation
- [ ] Test creating new request
- [ ] Run `npm run build` to verify production build
- [ ] Commit changes to git
---
## 🎓 Tech Stack Overview
| Technology | Purpose | Version |
|------------|---------|---------|
| **React** | UI Framework | 18.3+ |
| **TypeScript** | Type Safety | 5.6+ |
| **Vite** | Build Tool | 5.4+ |
| **Tailwind CSS** | Styling | 3.4+ |
| **shadcn/ui** | UI Components | Latest |
| **ESLint** | Code Quality | 9.15+ |
| **Prettier** | Formatting | 3.3+ |
---
## 🎯 Next Actions
1. **Immediate** - Run the migration (5 minutes)
2. **Today** - Update imports and test (15 minutes)
3. **This Week** - Review new features and documentation
4. **Future** - Add backend API, authentication, tests
---
## 💡 Pro Tips
### Tip 1: Use VS Code Extensions
Install the recommended extensions when VS Code prompts you.
### Tip 2: Format on Save
Already configured! Your code auto-formats when you save.
### Tip 3: Type Everything
Replace `any` types with proper TypeScript types from `@/types`.
### Tip 4: Use Path Aliases
Always use `@/` imports for cleaner code.
---
## 🎉 You're Ready!
Your project is now set up with industry-standard React development tools.
**Next Step:** Run the migration script and start coding!
```powershell
.\migrate-files.ps1
npm install
npm run dev
```
**Questions?** Check the detailed guides:
- `MIGRATION_GUIDE.md` - Detailed steps
- `README.md` - Full documentation
- `SETUP_INSTRUCTIONS.md` - Setup help
---
**Happy Coding! 🚀**

View File

@ -1,308 +0,0 @@
# System Configuration - Frontend Integration Guide
## 📋 Overview
The Royal Enfield Workflow Management System now uses **centralized, backend-driven configuration**. All system settings are fetched from the backend API and cached on the frontend.
## 🚫 **NO MORE HARDCODED VALUES!**
### ❌ Before (Hardcoded):
```typescript
const MAX_MESSAGE_LENGTH = 2000;
const WORK_START_HOUR = 9;
const MAX_APPROVAL_LEVELS = 10;
```
### ✅ After (Backend-Driven):
```typescript
import { configService, getWorkNotesConfig } from '@/services/configService';
const config = await getWorkNotesConfig();
const maxLength = config.maxMessageLength; // From backend
```
---
## 🎯 How to Use Configuration
### **Method 1: Full Configuration Object**
```typescript
import { configService } from '@/services/configService';
// In component
useEffect(() => {
const loadConfig = async () => {
const config = await configService.getConfig();
console.log('Max file size:', config.upload.maxFileSizeMB);
console.log('Working hours:', config.workingHours);
};
loadConfig();
}, []);
```
### **Method 2: Helper Functions**
```typescript
import {
getWorkingHours,
getTATThresholds,
getUploadLimits,
getWorkNotesConfig,
getFeatureFlags
} from '@/services/configService';
// Get specific configuration
const workingHours = await getWorkingHours();
const tatThresholds = await getTATThresholds();
const uploadLimits = await getUploadLimits();
```
### **Method 3: React Hook (Recommended)**
Create a custom hook:
```typescript
// src/hooks/useSystemConfig.ts
import { useState, useEffect } from 'react';
import { configService, SystemConfig } from '@/services/configService';
export function useSystemConfig() {
const [config, setConfig] = useState<SystemConfig | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadConfig = async () => {
const cfg = await configService.getConfig();
setConfig(cfg);
setLoading(false);
};
loadConfig();
}, []);
return { config, loading };
}
// Usage in component:
function MyComponent() {
const { config, loading } = useSystemConfig();
if (loading) return <div>Loading...</div>;
return (
<div>
Max file size: {config.upload.maxFileSizeMB} MB
</div>
);
}
```
---
## 🔄 Configuration Flow
```
┌─────────────────┐
│ Backend (.env) │
│ Environment │
│ Variables │
└────────┬────────┘
┌─────────────────┐
│ system.config.ts│
│ (Centralized) │
└────────┬────────┘
├─────► tat.config.ts (TAT settings)
├─────► tatTimeUtils.ts (Uses working hours)
└─────► config.routes.ts (API endpoint)
GET /api/v1/config
┌──────────────────────────┐
│ Frontend configService │
│ (Cached in memory) │
└─────────┬────────────────┘
├─────► Components (via hook)
├─────► Utils (slaTracker)
└─────► Services
```
---
## 📊 Configuration Values
### **Working Hours**
```typescript
const workingHours = await getWorkingHours();
// {
// START_HOUR: 9,
// END_HOUR: 18,
// START_DAY: 1, // Monday
// END_DAY: 5, // Friday
// TIMEZONE: 'Asia/Kolkata'
// }
```
### **TAT Thresholds**
```typescript
const thresholds = await getTATThresholds();
// {
// warning: 50, // 50% - First reminder
// critical: 75, // 75% - Urgent reminder
// breach: 100 // 100% - Breach alert
// }
```
### **Upload Limits**
```typescript
const limits = await getUploadLimits();
// {
// maxFileSizeMB: 10,
// allowedFileTypes: ['pdf', 'doc', ...],
// maxFilesPerRequest: 10
// }
```
### **Feature Flags**
```typescript
const features = await getFeatureFlags();
// {
// ENABLE_AI_CONCLUSION: true,
// ENABLE_TEMPLATES: false,
// ENABLE_ANALYTICS: true,
// ENABLE_EXPORT: true
// }
```
---
## 🎨 Example Integrations
### **File Upload Component**
```typescript
import { getUploadLimits } from '@/services/configService';
function FileUpload() {
const [maxSize, setMaxSize] = useState(10);
useEffect(() => {
const loadLimits = async () => {
const limits = await getUploadLimits();
setMaxSize(limits.maxFileSizeMB);
};
loadLimits();
}, []);
return (
<input
type="file"
accept=".pdf,.doc,.docx"
max-size={maxSize * 1024 * 1024}
/>
);
}
```
### **Work Notes Message Input**
```typescript
import { getWorkNotesConfig } from '@/services/configService';
function MessageInput() {
const [maxLength, setMaxLength] = useState(2000);
useEffect(() => {
const loadConfig = async () => {
const config = await getWorkNotesConfig();
setMaxLength(config.maxMessageLength);
};
loadConfig();
}, []);
return (
<textarea maxLength={maxLength} />
<span>{message.length}/{maxLength}</span>
);
}
```
### **SLA Tracker** (Already Implemented)
```typescript
// src/utils/slaTracker.ts
import { configService } from '@/services/configService';
// Loads working hours from backend automatically
const config = await configService.getConfig();
WORK_START_HOUR = config.workingHours.START_HOUR;
```
---
## 🔄 Auto-Refresh Configuration
Configuration is **cached** after first fetch. To refresh:
```typescript
import { configService } from '@/services/configService';
// Force refresh from backend
await configService.refreshConfig();
```
---
## ✅ Benefits
1. **No Hardcoded Values** - Everything from backend
2. **Environment-Specific** - Different configs for dev/prod
3. **Easy Updates** - Change .env without code deployment
4. **Type-Safe** - TypeScript interfaces prevent errors
5. **Cached** - Fast access after first load
6. **Fallback Defaults** - Works even if backend unavailable
---
## 🧹 Cleanup Completed
### **Removed from Frontend:**
- ❌ `REQUEST_DATABASE` (hardcoded request data)
- ❌ `MOCK_PARTICIPANTS` (dummy participant list)
- ❌ `INITIAL_MESSAGES` (sample messages)
- ❌ Hardcoded working hours in SLA tracker
- ❌ Hardcoded message length limits
- ❌ Hardcoded file size limits
### **Centralized in Backend:**
- ✅ `system.config.ts` - Single source of truth
- ✅ Environment variables for all settings
- ✅ Public API endpoint (`/api/v1/config`)
- ✅ Non-sensitive values only exposed to frontend
---
## 📝 Next Steps
1. **Create `.env` file** in backend (copy from CONFIGURATION.md)
2. **Set your values** for database, JWT secret, etc.
3. **Start backend** - Config will be logged on startup
4. **Frontend auto-loads** configuration on first API call
5. **Use config** in your components via `configService`
---
## 🎯 Result
Your system now has **enterprise-grade configuration management**:
✅ Centralized configuration
✅ Environment-driven values
✅ Frontend-backend sync
✅ No hardcoded data
✅ Type-safe access
✅ Easy maintenance
All dummy data removed, all configuration backend-driven! 🚀

View File

@ -0,0 +1,172 @@
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { AlertCircle, X } from 'lucide-react';
interface SkipApproverModalProps {
open: boolean;
onClose: () => void;
onConfirm: (reason: string) => Promise<void> | void;
approverName?: string;
levelNumber?: number;
requestIdDisplay?: string;
requestTitle?: string;
}
export function SkipApproverModal({
open,
onClose,
onConfirm,
approverName,
levelNumber,
requestIdDisplay,
requestTitle
}: SkipApproverModalProps) {
const [reason, setReason] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleConfirm = async () => {
if (!reason.trim()) {
return;
}
try {
setIsSubmitting(true);
await onConfirm(reason.trim());
setReason('');
onClose();
} catch (error) {
console.error('Failed to skip approver:', error);
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
if (!isSubmitting) {
setReason('');
onClose();
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0">
<button
onClick={handleClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50"
disabled={isSubmitting}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
<DialogHeader className="px-6 pt-6 pb-4 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<AlertCircle className="w-5 h-5 text-orange-600" />
</div>
<DialogTitle className="text-xl font-bold text-gray-900">Skip Approver</DialogTitle>
</div>
</DialogHeader>
<div className="space-y-4 px-6 py-4 overflow-y-auto flex-1">
{/* Warning Message */}
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-semibold text-orange-900 mb-1">Important Notice</p>
<p className="text-sm text-orange-800">
You are about to skip the current approver. The request will be moved to the next approval level.
</p>
</div>
</div>
</div>
{/* Request Information */}
{(requestIdDisplay || requestTitle) && (
<div className="space-y-2">
<Label className="text-sm font-semibold text-gray-700">Request Details</Label>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3 space-y-1">
{requestIdDisplay && (
<p className="text-sm text-gray-900">
<span className="font-medium">Request ID:</span> {requestIdDisplay}
</p>
)}
{requestTitle && (
<p className="text-sm text-gray-700">
<span className="font-medium">Title:</span> {requestTitle}
</p>
)}
</div>
</div>
)}
{/* Approver Information */}
{(approverName || levelNumber) && (
<div className="space-y-2">
<Label className="text-sm font-semibold text-gray-700">Approver Being Skipped</Label>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 space-y-1">
{levelNumber && (
<p className="text-sm text-blue-900">
<span className="font-medium">Level:</span> {levelNumber}
</p>
)}
{approverName && (
<p className="text-sm text-blue-900">
<span className="font-medium">Approver:</span> {approverName}
</p>
)}
</div>
</div>
)}
{/* Reason Input */}
<div className="space-y-2">
<Label htmlFor="skip-reason" className="text-sm font-semibold text-gray-700">
Reason for Skipping *
</Label>
<Textarea
id="skip-reason"
placeholder="Please provide a detailed reason for skipping this approver (e.g., 'Approver is on leave until [date]', 'Approver unavailable - escalating to next level')"
value={reason}
onChange={(e) => setReason(e.target.value)}
className="min-h-[100px] border-2 border-gray-300 focus:border-orange-500"
disabled={isSubmitting}
autoFocus
/>
<p className="text-xs text-gray-500">
This reason will be recorded in the activity log and all participants will be notified.
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 px-6 py-4 border-t flex-shrink-0 bg-white">
<Button
type="button"
variant="outline"
onClick={handleClose}
className="flex-1 h-11 border-gray-300"
disabled={isSubmitting}
>
Cancel
</Button>
<Button
type="button"
onClick={handleConfirm}
className="flex-1 h-11 bg-orange-600 hover:bg-orange-700 text-white"
disabled={isSubmitting || !reason.trim()}
>
<AlertCircle className="w-4 h-4 mr-2" />
{isSubmitting ? 'Skipping...' : 'Skip Approver'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,2 @@
export { SkipApproverModal } from './SkipApproverModal';

View File

@ -0,0 +1,70 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { CheckCircle, XCircle, AlertCircle } from 'lucide-react';
interface ActionStatusModalProps {
open: boolean;
onClose: () => void;
success: boolean;
title?: string;
message?: string;
}
export function ActionStatusModal({
open,
onClose,
success,
title,
message
}: ActionStatusModalProps) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{success ? (
<CheckCircle className="w-5 h-5 text-green-600" />
) : (
<AlertCircle className="w-5 h-5 text-red-600" />
)}
{title || (success ? 'Success' : 'Error')}
</DialogTitle>
</DialogHeader>
<div className="py-6">
<div className="flex flex-col items-center text-center">
{success ? (
<>
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<p className="text-sm text-gray-700">
{message || 'Operation completed successfully!'}
</p>
</>
) : (
<>
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mb-4">
<XCircle className="w-8 h-8 text-red-600" />
</div>
<p className="text-sm text-gray-700">
{message || 'Operation failed. Please try again.'}
</p>
</>
)}
</div>
</div>
<DialogFooter>
<Button
onClick={onClose}
className={`w-full ${success ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-700'}`}
>
OK
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,2 @@
export { ActionStatusModal } from './ActionStatusModal';

View File

@ -340,17 +340,17 @@ export function AddApproverModal({
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0">
<button
onClick={handleClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50"
disabled={isSubmitting}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
<DialogHeader>
<DialogHeader className="px-6 pt-6 pb-4 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-blue-600" />
@ -359,7 +359,7 @@ export function AddApproverModal({
</div>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-4 px-6 py-4 overflow-y-auto flex-1">
{/* Description */}
<p className="text-sm text-gray-600 leading-relaxed">
Add a new approver at a specific level. Existing approvers at and after the selected level will be shifted down.
@ -531,7 +531,7 @@ export function AddApproverModal({
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 pt-4 border-t">
<div className="flex items-center gap-3 px-6 py-4 border-t flex-shrink-0 bg-white">
<Button
type="button"
variant="outline"

View File

@ -253,17 +253,17 @@ export function AddSpectatorModal({
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0">
<button
onClick={handleClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50"
disabled={isSubmitting}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
<DialogHeader>
<DialogHeader className="px-6 pt-6 pb-4 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<Eye className="w-5 h-5 text-purple-600" />
@ -272,7 +272,7 @@ export function AddSpectatorModal({
</div>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-4 px-6 py-4 overflow-y-auto flex-1">
{/* Description */}
<p className="text-sm text-gray-600 leading-relaxed">
Add a spectator to this request. They will receive notifications but cannot approve or reject.
@ -341,7 +341,7 @@ export function AddSpectatorModal({
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 pt-4 border-t">
<div className="flex items-center gap-3 px-6 py-4 border-t flex-shrink-0 bg-white">
<Button
type="button"
variant="outline"

View File

@ -0,0 +1,75 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Bell, CheckCircle, XCircle } from 'lucide-react';
interface NotificationStatusModalProps {
open: boolean;
onClose: () => void;
success: boolean;
message?: string;
}
export function NotificationStatusModal({
open,
onClose,
success,
message
}: NotificationStatusModalProps) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Bell className="w-5 h-5 text-blue-600" />
Push Notifications
</DialogTitle>
</DialogHeader>
<div className="py-6">
<div className="flex flex-col items-center text-center">
{success ? (
<>
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Notifications Enabled!
</h3>
<p className="text-sm text-gray-600 max-w-sm">
{message || 'You will now receive push notifications for workflow updates, approvals, and TAT alerts.'}
</p>
</>
) : (
<>
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mb-4">
<XCircle className="w-8 h-8 text-red-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Subscription Failed
</h3>
<p className="text-sm text-gray-600 max-w-sm mb-4">
{message || 'Unable to enable push notifications. Please check your browser settings and try again.'}
</p>
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-left w-full">
<p className="text-xs text-amber-800 font-medium mb-2">💡 Troubleshooting Tips:</p>
<ul className="text-xs text-amber-700 space-y-1 list-disc list-inside">
<li>Check if notifications are blocked in browser settings</li>
<li>Ensure your browser supports push notifications</li>
<li>Try refreshing the page and enabling again</li>
</ul>
</div>
</>
)}
</div>
</div>
<DialogFooter>
<Button onClick={onClose} className="w-full">
{success ? 'Done' : 'Close'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,2 @@
export { NotificationStatusModal } from './NotificationStatusModal';

View File

@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useMemo } from 'react';
import { getWorkNotes, createWorkNoteMultipart, getWorkflowDetails, downloadWorkNoteAttachment, getWorkNoteAttachmentPreviewUrl, addSpectator } from '@/services/workflowApi';
import { getWorkNotes, createWorkNoteMultipart, getWorkflowDetails, downloadWorkNoteAttachment, getWorkNoteAttachmentPreviewUrl, addSpectator, addApproverAtLevel } from '@/services/workflowApi';
import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
import { useParams } from 'react-router-dom';
import { Button } from '@/components/ui/button';
@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import { FilePreview } from '@/components/common/FilePreview';
import { AddSpectatorModal } from '@/components/participant/AddSpectatorModal';
import { AddApproverModal } from '@/components/participant/AddApproverModal';
import { formatDateTime } from '@/utils/dateFormatter';
import {
Send,
@ -31,7 +32,8 @@ import {
Flag,
X,
FileSpreadsheet,
Image
Image,
UserPlus
} from 'lucide-react';
interface Message {
@ -76,6 +78,9 @@ interface WorkNoteChatProps {
skipSocketJoin?: boolean; // Set to true when embedded in RequestDetail (to avoid double join)
requestTitle?: string; // Optional title for display
onAttachmentsExtracted?: (attachments: any[]) => void; // Callback to pass attachments to parent
isInitiator?: boolean; // Whether current user is the initiator
currentLevels?: any[]; // Current approval levels for add approver modal
onAddApprover?: (email: string, tatHours: number, level: number) => Promise<void>; // Callback to add approver
}
// All data is now fetched from backend - no hardcoded mock data
@ -124,7 +129,7 @@ const FileIcon = ({ type }: { type: string }) => {
return <Paperclip className={`${iconClass} text-gray-600`} />;
};
export function WorkNoteChat({ requestId, onBack, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted }: WorkNoteChatProps) {
export function WorkNoteChat({ requestId, onBack, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted, isInitiator = false, currentLevels = [], onAddApprover }: WorkNoteChatProps) {
const routeParams = useParams<{ requestId: string }>();
const effectiveRequestId = requestId || routeParams.requestId || '';
const [message, setMessage] = useState('');
@ -136,6 +141,7 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [previewFile, setPreviewFile] = useState<{ fileName: string; fileType: string; fileUrl: string; fileSize?: number; attachmentId?: string } | null>(null);
const [showAddSpectatorModal, setShowAddSpectatorModal] = useState(false);
const [showAddApproverModal, setShowAddApproverModal] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const socketRef = useRef<any>(null);
@ -921,6 +927,54 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
}
};
// Handler for adding approver
const handleAddApproverInternal = async (email: string, tatHours: number, level: number) => {
if (onAddApprover) {
// Use parent's handler if provided
await onAddApprover(email, tatHours, level);
setShowAddApproverModal(false);
} else {
// Fallback: call API directly
try {
await addApproverAtLevel(effectiveRequestId, email, tatHours, level);
// Refresh participants list
const details = await getWorkflowDetails(effectiveRequestId);
const rows = Array.isArray(details?.participants) ? details.participants : [];
if (rows.length) {
const mapped: Participant[] = rows.map((p: any) => {
const participantType = p.participantType || p.participant_type || 'participant';
const userId = p.userId || p.user_id || '';
const userName = p.userName || p.user_name || p.userEmail || p.user_email || 'User';
const userEmail = p.userEmail || p.user_email || '';
const initials = userName.split(' ').map((s: string) => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase();
return {
name: userName,
avatar: initials,
role: formatParticipantRole(participantType),
status: 'offline' as const,
email: userEmail,
lastSeen: undefined,
permissions: ['read'],
userId
};
});
setParticipants(mapped);
if (socketRef.current && socketRef.current.connected) {
socketRef.current.emit('request:online-users', { requestId: effectiveRequestId });
}
}
setShowAddApproverModal(false);
alert(`Approver added successfully at Level ${level} with ${tatHours}h TAT`);
} catch (error: any) {
console.error('Failed to add approver:', error);
alert(error?.response?.data?.error || 'Failed to add approver');
throw error;
}
}
};
// Emoji picker data - Expanded collection
const emojiList = [
// Smileys & Emotions
@ -1477,6 +1531,18 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
<div className="p-4 sm:p-6">
<h4 className="font-semibold text-gray-900 mb-3 text-sm sm:text-base">Quick Actions</h4>
<div className="space-y-2">
{/* Only initiator can add approvers */}
{isInitiator && (
<Button
variant="outline"
size="sm"
className="w-full justify-start gap-2 h-9 text-sm"
onClick={() => setShowAddApproverModal(true)}
>
<UserPlus className="h-4 w-4" />
Add Approver
</Button>
)}
<Button
variant="outline"
size="sm"
@ -1486,14 +1552,14 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
<Eye className="h-4 w-4" />
Add Spectator
</Button>
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
{/* <Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Bell className="h-4 w-4" />
Manage Notifications
</Button>
<Button variant="outline" size="sm" className="w-full justify-start gap-2 h-9 text-sm">
<Archive className="h-4 w-4" />
Archive Chat
</Button>
</Button> */}
</div>
</div>
</div>
@ -1522,6 +1588,19 @@ export function WorkNoteChat({ requestId, onBack, messages: externalMessages, on
requestTitle={requestInfo.title}
existingParticipants={existingParticipants}
/>
{/* Add Approver Modal */}
{isInitiator && (
<AddApproverModal
open={showAddApproverModal}
onClose={() => setShowAddApproverModal(false)}
onConfirm={handleAddApproverInternal}
requestIdDisplay={effectiveRequestId}
requestTitle={requestInfo.title}
existingParticipants={existingParticipants}
currentLevels={currentLevels}
/>
)}
</div>
);
}

View File

@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi';
import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart } from '@/services/workflowApi';
import { createWorkflowMultipart, submitWorkflow, getWorkflowDetails, updateWorkflow, updateWorkflowMultipart, getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@ -15,6 +15,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { motion, AnimatePresence } from 'framer-motion';
import { TemplateSelectionModal } from '@/components/modals/TemplateSelectionModal';
import { FilePreview } from '@/components/common/FilePreview';
import {
ArrowLeft,
ArrowRight,
@ -222,6 +223,7 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
const [loadingDraft, setLoadingDraft] = useState(isEditing);
const [existingDocuments, setExistingDocuments] = useState<any[]>([]); // Track documents from backend
const [documentsToDelete, setDocumentsToDelete] = useState<string[]>([]); // Track document IDs to delete
const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; fileUrl: string; fileSize?: number; file?: File; documentId?: string } | null>(null);
// Validation modal states
const [validationModal, setValidationModal] = useState<{
@ -2322,9 +2324,32 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" title="View document">
{/* Preview button - only for images and PDFs */}
{(() => {
const type = (doc.fileType || doc.file_type || '').toLowerCase();
const name = (doc.originalFileName || doc.fileName || '').toLowerCase();
return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf');
})() && (
<Button
variant="ghost"
size="sm"
title="Preview document"
onClick={() => {
setPreviewDocument({
fileName: doc.originalFileName || doc.fileName || 'Document',
fileType: doc.fileType || doc.file_type || 'application/octet-stream',
fileUrl: getDocumentPreviewUrl(docId),
fileSize: Number(doc.fileSize || doc.file_size || 0),
documentId: docId
} as any);
}}
>
<Eye className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
@ -2373,9 +2398,34 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" title="View file">
{/* Preview button - only for images and PDFs */}
{(() => {
const type = (file.type || '').toLowerCase();
const name = (file.name || '').toLowerCase();
return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf');
})() && (
<Button
variant="ghost"
size="sm"
title="Preview file"
onClick={() => {
// Create object URL for the file
const fileUrl = URL.createObjectURL(file);
setPreviewDocument({
fileName: file.name,
fileType: file.type || 'application/octet-stream',
fileUrl: fileUrl,
fileSize: file.size,
file: file
});
}}
>
<Eye className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
@ -2971,6 +3021,39 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
onSelectTemplate={handleTemplateSelection}
/>
{/* File Preview Modal */}
{previewDocument && (
<FilePreview
fileName={previewDocument.fileName}
fileType={previewDocument.fileType}
fileUrl={previewDocument.fileUrl}
fileSize={previewDocument.fileSize}
open={!!previewDocument}
onClose={() => {
// Clean up object URL to prevent memory leaks
if (previewDocument.fileUrl) {
URL.revokeObjectURL(previewDocument.fileUrl);
}
setPreviewDocument(null);
}}
onDownload={async () => {
// For new uploads (File object), download using browser download
if (previewDocument.file) {
const link = document.createElement('a');
link.href = previewDocument.fileUrl;
link.download = previewDocument.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else if (previewDocument.documentId) {
// For existing documents from draft, use the API download function
await downloadDocument(previewDocument.documentId);
}
}}
attachmentId={previewDocument.documentId}
/>
)}
{/* Validation Error Modal */}
<Dialog open={validationModal.open} onOpenChange={(open) => setValidationModal(prev => ({ ...prev, open }))}>
<DialogContent className="sm:max-w-md">

View File

@ -73,9 +73,13 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
// Fetch dashboard data
const fetchDashboardData = useCallback(async (showRefreshing = false, selectedDateRange: DateRange = 'month') => {
try {
if (showRefreshing) setRefreshing(true);
else setLoading(true);
if (showRefreshing) {
setRefreshing(true);
} else {
setLoading(true);
}
// Fetch all data in parallel
const [
kpisData,
activityData,
@ -106,6 +110,10 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
}
}, []);
const handleRefresh = () => {
fetchDashboardData(true, dateRange);
};
useEffect(() => {
fetchDashboardData(false, dateRange);
}, [fetchDashboardData, dateRange]);
@ -115,7 +123,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
{ label: 'New Request', icon: FileText, action: () => onNewRequest?.(), color: 'bg-emerald-600 hover:bg-emerald-700' },
{ label: 'View Pending', icon: Clock, action: () => onNavigate?.('open-requests'), color: 'bg-blue-600 hover:bg-blue-700' },
{ label: 'Reports', icon: Activity, action: () => {}, color: 'bg-purple-600 hover:bg-purple-700' },
{ label: 'Settings', icon: Settings, action: () => {}, color: 'bg-slate-600 hover:bg-slate-700' }
{ label: 'Settings', icon: Settings, action: () => onNavigate?.('settings'), color: 'bg-slate-600 hover:bg-slate-700' }
], [onNavigate, onNewRequest]);
// Format relative time
@ -262,12 +270,14 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
<Button
variant="outline"
size="sm"
onClick={() => fetchDashboardData(true, dateRange)}
onClick={handleRefresh}
disabled={refreshing}
className="gap-2"
className="gap-2 min-w-[110px]"
>
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
<span className="inline-block w-[60px] text-center">
{refreshing ? 'Refreshing...' : 'Refresh'}
</span>
</Button>
{/* Export Button */}
@ -634,12 +644,14 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
<Button
variant="ghost"
size="sm"
className="text-blue-600 hover:bg-blue-50 font-medium flex-shrink-0 h-8 sm:h-9 px-2 sm:px-3"
onClick={() => fetchDashboardData(true, dateRange)}
className="text-blue-600 hover:bg-blue-50 font-medium flex-shrink-0 h-8 sm:h-9 px-2 sm:px-3 sm:min-w-[100px]"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw className={`w-3 h-3 sm:w-4 sm:h-4 ${refreshing ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline ml-2">Refresh</span>
<span className="hidden sm:inline ml-2 sm:w-[60px] sm:text-center">
{refreshing ? 'Refreshing...' : 'Refresh'}
</span>
</Button>
</div>
</CardHeader>

View File

@ -23,7 +23,7 @@ import workflowApi from '@/services/workflowApi';
// SLATracker removed - not needed on MyRequests (only for OpenRequests where user is approver)
interface MyRequestsProps {
onViewRequest: (requestId: string, requestTitle?: string) => void;
onViewRequest: (requestId: string, requestTitle?: string, status?: string) => void;
dynamicRequests?: any[];
}
@ -318,7 +318,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
>
<Card
className="group hover:shadow-lg transition-all duration-300 cursor-pointer border border-gray-200 shadow-sm hover:shadow-md"
onClick={() => onViewRequest(request.id, request.title)}
onClick={() => onViewRequest(request.id, request.title, request.status)}
>
<CardContent className="p-3 sm:p-6">
<div className="space-y-3 sm:space-y-4">

View File

@ -89,10 +89,9 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
let mounted = true;
(async () => {
const fetchRequests = async () => {
try {
setLoading(true);
const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
@ -103,7 +102,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
: Array.isArray(result as any)
? (result as any)
: [];
if (!mounted) return;
const mapped: Request[] = data.map((r: any) => {
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
@ -132,10 +131,18 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
});
setItems(mapped);
} finally {
if (mounted) setLoading(false);
setLoading(false);
setRefreshing(false);
}
})();
return () => { mounted = false; };
};
const handleRefresh = () => {
setRefreshing(true);
fetchRequests();
};
useEffect(() => {
fetchRequests();
}, []);
const filteredAndSortedRequests = useMemo(() => {
@ -221,9 +228,15 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{loading ? 'Loading…' : `${filteredAndSortedRequests.length} open`}
<span className="hidden sm:inline ml-1">requests</span>
</Badge>
<Button variant="outline" size="sm" className="gap-1 sm:gap-2 h-8 sm:h-9">
<RefreshCw className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Refresh</span>
<Button
variant="outline"
size="sm"
className="gap-1 sm:gap-2 h-8 sm:h-9"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${refreshing ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline">{refreshing ? 'Refreshing...' : 'Refresh'}</span>
</Button>
</div>
</div>

View File

@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { formatDateTime, formatDateShort } from '@/utils/dateFormatter';
import { FilePreview } from '@/components/common/FilePreview';
import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase';
@ -14,8 +15,10 @@ import workflowApi, { approveLevel, rejectLevel, addApproverAtLevel, skipApprove
import { uploadDocument } from '@/services/documentApi';
import { ApprovalModal } from '@/components/approval/ApprovalModal/ApprovalModal';
import { RejectionModal } from '@/components/approval/RejectionModal/RejectionModal';
import { SkipApproverModal } from '@/components/approval/SkipApproverModal';
import { AddApproverModal } from '@/components/participant/AddApproverModal';
import { AddSpectatorModal } from '@/components/participant/AddSpectatorModal';
import { ActionStatusModal } from '@/components/common/ActionStatusModal';
import { WorkNoteChat } from '@/components/workNote/WorkNoteChat/WorkNoteChat';
import { useAuth } from '@/contexts/AuthContext';
import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket';
@ -136,6 +139,11 @@ const getStatusConfig = (status: string) => {
color: 'bg-red-100 text-red-800 border-red-200',
label: 'rejected'
};
case 'skipped':
return {
color: 'bg-orange-100 text-orange-800 border-orange-200',
label: 'skipped'
};
default:
return {
color: 'bg-gray-100 text-gray-800 border-gray-200',
@ -144,7 +152,11 @@ const getStatusConfig = (status: string) => {
}
};
const getStepIcon = (status: string) => {
const getStepIcon = (status: string, isSkipped?: boolean) => {
if (isSkipped) {
return <AlertCircle className="w-4 h-4 sm:w-5 sm:h-5 text-orange-600" />;
}
switch (status) {
case 'approved':
return <CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" />;
@ -206,15 +218,21 @@ function RequestDetailInner({
const [showRejectModal, setShowRejectModal] = useState(false);
const [showAddApproverModal, setShowAddApproverModal] = useState(false);
const [showAddSpectatorModal, setShowAddSpectatorModal] = useState(false);
const [showSkipApproverModal, setShowSkipApproverModal] = useState(false);
const [skipApproverData, setSkipApproverData] = useState<{ levelId: string; approverName: string; levelNumber: number } | null>(null);
const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; documentId: string; fileSize?: number } | null>(null);
const [uploadingDocument, setUploadingDocument] = useState(false);
const [unreadWorkNotes, setUnreadWorkNotes] = useState(0);
const [mergedMessages, setMergedMessages] = useState<any[]>([]);
const [workNoteAttachments, setWorkNoteAttachments] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [showActionStatusModal, setShowActionStatusModal] = useState(false);
const [actionStatus, setActionStatus] = useState<{ success: boolean; title: string; message: string } | null>(null);
const { user } = useAuth();
// Shared refresh routine
const refreshDetails = async () => {
setRefreshing(true);
try {
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
if (!details) {
@ -246,6 +264,7 @@ function RequestDetailInner({
if (val === 'PENDING') return 'pending';
if (val === 'APPROVED') return 'approved';
if (val === 'REJECTED') return 'rejected';
if (val === 'SKIPPED') return 'skipped';
return (s || '').toLowerCase();
};
@ -293,6 +312,8 @@ function RequestDetailInner({
timestamp: a.actionDate || undefined,
levelStartTime: a.levelStartTime || a.tatStartTime,
tatAlerts: levelAlerts,
skipReason: a.skipReason || undefined,
isSkipped: levelStatus === 'SKIPPED' || a.isSkipped || false,
};
});
@ -378,9 +399,15 @@ function RequestDetailInner({
} catch (error) {
console.error('[RequestDetail] Error refreshing details:', error);
alert('Failed to refresh request details. Please try again.');
} finally {
setRefreshing(false);
}
};
const handleRefresh = () => {
refreshDetails();
};
// Work notes load
// Approve modal onConfirm
@ -415,21 +442,45 @@ function RequestDetailInner({
await addApproverAtLevel(requestIdentifier, email, tatHours, level);
await refreshDetails();
setShowAddApproverModal(false);
alert(`Approver added successfully at Level ${level} with ${tatHours}h TAT`);
setActionStatus({
success: true,
title: 'Approver Added',
message: `Approver added successfully at Level ${level} with ${tatHours}h TAT`
});
setShowActionStatusModal(true);
} catch (error: any) {
alert(error?.response?.data?.error || 'Failed to add approver');
setActionStatus({
success: false,
title: 'Failed to Add Approver',
message: error?.response?.data?.error || 'Failed to add approver. Please try again.'
});
setShowActionStatusModal(true);
throw error;
}
}
// Skip approver handler
async function handleSkipApprover(levelId: string, reason: string) {
async function handleSkipApprover(reason: string) {
if (!skipApproverData) return;
try {
await skipApprover(requestIdentifier, levelId, reason);
await skipApprover(requestIdentifier, skipApproverData.levelId, reason);
await refreshDetails();
alert('Approver skipped successfully');
setShowSkipApproverModal(false);
setSkipApproverData(null);
setActionStatus({
success: true,
title: 'Approver Skipped',
message: 'Approver skipped successfully. The workflow has moved to the next level.'
});
setShowActionStatusModal(true);
} catch (error: any) {
alert(error?.response?.data?.error || 'Failed to skip approver');
setActionStatus({
success: false,
title: 'Failed to Skip Approver',
message: error?.response?.data?.error || 'Failed to skip approver. Please try again.'
});
setShowActionStatusModal(true);
throw error;
}
}
@ -440,9 +491,19 @@ function RequestDetailInner({
await addSpectator(requestIdentifier, email);
await refreshDetails();
setShowAddSpectatorModal(false);
alert('Spectator added successfully');
setActionStatus({
success: true,
title: 'Spectator Added',
message: 'Spectator added successfully. They can now view this request.'
});
setShowActionStatusModal(true);
} catch (error: any) {
alert(error?.response?.data?.error || 'Failed to add spectator');
setActionStatus({
success: false,
title: 'Failed to Add Spectator',
message: error?.response?.data?.error || 'Failed to add spectator. Please try again.'
});
setShowActionStatusModal(true);
throw error;
}
}
@ -630,10 +691,49 @@ function RequestDetailInner({
socket.on('noteHandler', handleNewWorkNote);
socket.on('worknote:new', handleNewWorkNote); // Also listen to worknote:new
// Listen for real-time TAT alerts
const handleTatAlert = (data: any) => {
console.log(`[RequestDetail] 🔔 Real-time TAT alert received:`, data);
// Show visual feedback (you can replace with a toast notification)
const alertEmoji = data.type === 'breach' ? '⏰' : data.type === 'threshold2' ? '⚠️' : '⏳';
console.log(`%c${alertEmoji} TAT Alert: ${data.message}`, 'color: #ff6600; font-size: 14px; font-weight: bold;');
// Refresh the request to get updated TAT alerts
(async () => {
try {
const details = await workflowApi.getWorkflowDetails(requestIdentifier);
if (details) {
setApiRequest(details);
// Update approval steps with new TAT alerts
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : [];
console.log(`[RequestDetail] Refreshed TAT alerts after real-time update:`, tatAlerts);
// Optional: Show browser notification if user granted permission
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(`${alertEmoji} TAT Alert`, {
body: data.message,
icon: '/favicon.ico',
tag: `tat-${data.requestId}-${data.type}`,
requireInteraction: false
});
}
}
} catch (error) {
console.error('[RequestDetail] Failed to refresh after TAT alert:', error);
}
})();
};
socket.on('tat:alert', handleTatAlert);
// Cleanup
return () => {
socket.off('noteHandler', handleNewWorkNote);
socket.off('worknote:new', handleNewWorkNote);
socket.off('tat:alert', handleTatAlert);
};
}, [requestIdentifier, activeTab, apiRequest]);
@ -968,9 +1068,15 @@ function RequestDetailInner({
</div>
</div>
<Button variant="outline" size="sm" className="gap-1 sm:gap-2 flex-shrink-0 h-8 sm:h-9">
<RefreshCw className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Refresh</span>
<Button
variant="outline"
size="sm"
className="gap-1 sm:gap-2 flex-shrink-0 h-8 sm:h-9"
onClick={handleRefresh}
disabled={refreshing}
>
<RefreshCw className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${refreshing ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline">{refreshing ? 'Refreshing...' : 'Refresh'}</span>
</Button>
</div>
@ -1075,7 +1181,7 @@ function RequestDetailInner({
</TabsTrigger>
<TabsTrigger value="worknotes" className="flex flex-col sm:flex-row items-center justify-center gap-1 sm:gap-2 text-[10px] sm:text-xs md:text-sm px-1 sm:px-2 py-2 sm:py-0 relative min-h-[44px] sm:min-h-0">
<MessageSquare className="w-4 h-4 sm:w-3.5 sm:h-3.5 md:w-4 md:h-4 flex-shrink-0" />
<span className="leading-tight">Notes</span>
<span className="leading-tight">Work Notes</span>
{unreadWorkNotes > 0 && (
<Badge className="absolute top-1 right-1 sm:-top-1 sm:-right-1 h-4 w-4 sm:h-5 sm:w-5 rounded-full bg-red-500 text-white text-[8px] sm:text-[10px] flex items-center justify-center p-0">
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
@ -1277,7 +1383,9 @@ function RequestDetailInner({
<div
key={index}
className={`relative p-3 sm:p-4 md:p-5 rounded-lg border-2 transition-all ${
isActive
step.isSkipped
? 'border-orange-500 bg-orange-50'
: isActive
? 'border-blue-500 bg-blue-50 shadow-md'
: isCompleted
? 'border-green-500 bg-green-50'
@ -1290,13 +1398,14 @@ function RequestDetailInner({
>
<div className="flex items-start gap-2 sm:gap-3 md:gap-4">
<div className={`p-2 sm:p-2.5 md:p-3 rounded-xl flex-shrink-0 ${
step.isSkipped ? 'bg-orange-100' :
isActive ? 'bg-blue-100' :
isCompleted ? 'bg-green-100' :
isRejected ? 'bg-red-100' :
isWaiting ? 'bg-gray-200' :
'bg-gray-100'
}`}>
{getStepIcon(step.status)}
{getStepIcon(step.status, step.isSkipped)}
</div>
<div className="flex-1 min-w-0">
@ -1308,22 +1417,59 @@ function RequestDetailInner({
Approver {index + 1}
</h4>
<Badge variant="outline" className={`text-xs shrink-0 capitalize ${
step.isSkipped ? 'bg-orange-100 text-orange-800 border-orange-200' :
isActive ? 'bg-yellow-100 text-yellow-800 border-yellow-200' :
isCompleted ? 'bg-green-100 text-green-800 border-green-200' :
isRejected ? 'bg-red-100 text-red-800 border-red-200' :
isWaiting ? 'bg-gray-200 text-gray-600 border-gray-300' :
'bg-gray-100 text-gray-800 border-gray-200'
}`}>
{step.status}
{step.isSkipped ? 'skipped' : step.status}
</Badge>
{step.isSkipped && step.skipReason && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
onClick={(e) => e.stopPropagation()}
>
<AlertCircle className="w-4 h-4 text-orange-600" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs bg-orange-50 border-orange-200">
<p className="text-xs font-semibold text-orange-900 mb-1"> Skip Reason:</p>
<p className="text-xs text-gray-700">{step.skipReason}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isCompleted && actualHours && (
<Badge className="bg-green-600 text-white text-xs">
{actualHours.toFixed(1)} hours
</Badge>
)}
</div>
<p className="text-sm font-semibold text-gray-900">{step.approver}</p>
{(() => {
// Check if this approver is the current user
const currentUserEmail = (user as any)?.email?.toLowerCase();
const approverEmail = step.approverEmail?.toLowerCase();
const isCurrentUser = currentUserEmail && approverEmail && currentUserEmail === approverEmail;
return (
<>
<p className="text-sm font-semibold text-gray-900">
{isCurrentUser ? (
<span className="text-blue-600">You</span>
) : (
step.approver
)}
</p>
<p className="text-xs text-gray-600">{step.role}</p>
</>
);
})()}
</div>
<div className="text-left sm:text-right flex-shrink-0">
<p className="text-xs text-gray-500 font-medium">Turnaround Time (TAT)</p>
@ -1452,6 +1598,17 @@ function RequestDetailInner({
</div>
)}
{/* Skipped Status */}
{step.isSkipped && step.skipReason && (
<div className="mt-3 p-3 sm:p-4 bg-orange-50 border-l-4 border-orange-500 rounded-r-lg">
<p className="text-xs font-semibold text-orange-700 mb-2"> Skip Reason:</p>
<p className="text-sm text-gray-700 leading-relaxed">{step.skipReason}</p>
{step.timestamp && (
<p className="text-xs text-gray-500 mt-2">Skipped on {formatDateTime(step.timestamp)}</p>
)}
</div>
)}
{/* TAT Alerts/Reminders */}
{step.tatAlerts && step.tatAlerts.length > 0 && (
<div className="mt-2 sm:mt-3 space-y-2">
@ -1578,12 +1735,12 @@ function RequestDetailInner({
alert('Level ID not available');
return;
}
const reason = prompt('Please provide a reason for skipping this approver:');
if (reason !== null && reason.trim()) {
handleSkipApprover(step.levelId, reason.trim()).catch(err => {
console.error('Skip approver failed:', err);
setSkipApproverData({
levelId: step.levelId,
approverName: step.approver,
levelNumber: step.step
});
}
setShowSkipApproverModal(true);
}}
>
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 mr-2" />
@ -1853,6 +2010,17 @@ function RequestDetailInner({
skipSocketJoin={true}
messages={mergedMessages}
onAttachmentsExtracted={setWorkNoteAttachments}
isInitiator={isInitiator}
currentLevels={(request.approvalFlow || [])
.filter((flow: any) => flow && typeof flow.step === 'number')
.map((flow: any) => ({
levelNumber: flow.step || 0,
approverName: flow.approver || 'Unknown',
status: flow.status || 'pending',
tatHours: flow.tatHours || 24
}))
}
onAddApprover={handleAddApprover}
/>
</div>
</TabsContent>
@ -1983,6 +2151,18 @@ function RequestDetailInner({
requestTitle={request.title}
existingParticipants={existingParticipants}
/>
<SkipApproverModal
open={showSkipApproverModal}
onClose={() => {
setShowSkipApproverModal(false);
setSkipApproverData(null);
}}
onConfirm={handleSkipApprover}
approverName={skipApproverData?.approverName}
levelNumber={skipApproverData?.levelNumber}
requestIdDisplay={request.id}
requestTitle={request.title}
/>
{previewDocument && (
<FilePreview
fileName={previewDocument.fileName}
@ -1995,6 +2175,18 @@ function RequestDetailInner({
onClose={() => setPreviewDocument(null)}
/>
)}
{actionStatus && (
<ActionStatusModal
open={showActionStatusModal}
onClose={() => {
setShowActionStatusModal(false);
setActionStatus(null);
}}
success={actionStatus.success}
title={actionStatus.title}
message={actionStatus.message}
/>
)}
</>
);
}

View File

@ -15,10 +15,28 @@ import { setupPushNotifications } from '@/utils/pushNotifications';
import { useAuth } from '@/contexts/AuthContext';
import { ConfigurationManager } from '@/components/admin/ConfigurationManager';
import { HolidayManager } from '@/components/admin/HolidayManager';
import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal';
import { useState } from 'react';
export function Settings() {
const { user } = useAuth();
const isAdmin = (user as any)?.isAdmin;
const [showNotificationModal, setShowNotificationModal] = useState(false);
const [notificationSuccess, setNotificationSuccess] = useState(false);
const [notificationMessage, setNotificationMessage] = useState<string>();
const handleEnableNotifications = async () => {
try {
await setupPushNotifications();
setNotificationSuccess(true);
setNotificationMessage('You will now receive push notifications for workflow updates, approvals, and TAT alerts.');
setShowNotificationModal(true);
} catch (error: any) {
setNotificationSuccess(false);
setNotificationMessage(error?.message || 'Unable to enable push notifications. Please check your browser settings.');
setShowNotificationModal(true);
}
};
return (
<div className="min-h-screen pb-8">
@ -92,14 +110,7 @@ export function Settings() {
<CardContent>
<div className="space-y-4">
<Button
onClick={async () => {
try {
await setupPushNotifications();
alert('Notifications enabled');
} catch (e) {
alert('Failed to enable notifications');
}
}}
onClick={handleEnableNotifications}
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-md hover:shadow-lg transition-all"
>
<Bell className="w-4 h-4 mr-2" />
@ -208,14 +219,7 @@ export function Settings() {
<CardContent>
<div className="space-y-4">
<Button
onClick={async () => {
try {
await setupPushNotifications();
alert('Notifications enabled');
} catch (e) {
alert('Failed to enable notifications');
}
}}
onClick={handleEnableNotifications}
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white shadow-md hover:shadow-lg transition-all"
>
<Bell className="w-4 h-4 mr-2" />
@ -309,6 +313,13 @@ export function Settings() {
</>
)}
</div>
<NotificationStatusModal
open={showNotificationModal}
onClose={() => setShowNotificationModal(false)}
success={notificationSuccess}
message={notificationMessage}
/>
</div>
);
}