reported bugs fixed
This commit is contained in:
parent
63738c529b
commit
61ba649ac4
@ -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.
|
||||
@ -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
|
||||
188
ERROR_FIX.md
188
ERROR_FIX.md
@ -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
|
||||
@ -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**
|
||||
|
||||
@ -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
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
342
START_HERE.md
342
START_HERE.md
@ -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! 🚀**
|
||||
|
||||
@ -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! 🚀
|
||||
|
||||
172
src/components/approval/SkipApproverModal/SkipApproverModal.tsx
Normal file
172
src/components/approval/SkipApproverModal/SkipApproverModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
2
src/components/approval/SkipApproverModal/index.ts
Normal file
2
src/components/approval/SkipApproverModal/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { SkipApproverModal } from './SkipApproverModal';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
2
src/components/common/ActionStatusModal/index.ts
Normal file
2
src/components/common/ActionStatusModal/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { ActionStatusModal } from './ActionStatusModal';
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
2
src/components/settings/NotificationStatusModal/index.ts
Normal file
2
src/components/settings/NotificationStatusModal/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { NotificationStatusModal } from './NotificationStatusModal';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 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">
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* 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"
|
||||
@ -2970,6 +3020,39 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
|
||||
onClose={() => setShowTemplateModal(false)}
|
||||
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 }))}>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -89,53 +89,60 @@ 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);
|
||||
|
||||
const fetchRequests = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
|
||||
const data = Array.isArray((result as any)?.data)
|
||||
? (result as any).data
|
||||
: Array.isArray((result as any)?.data?.data)
|
||||
? (result as any).data.data
|
||||
: Array.isArray(result as any)
|
||||
? (result as any)
|
||||
: [];
|
||||
|
||||
const mapped: Request[] = data.map((r: any) => {
|
||||
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
|
||||
|
||||
return {
|
||||
id: r.requestNumber || r.request_number || r.requestId,
|
||||
requestId: r.requestId,
|
||||
displayId: r.requestNumber || r.request_number || r.requestId,
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending',
|
||||
priority: (r.priority || '').toString().toLowerCase(),
|
||||
initiator: {
|
||||
name: (r.initiator?.displayName || r.initiator?.email || '—'),
|
||||
avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase())
|
||||
},
|
||||
currentApprover: r.currentApprover ? {
|
||||
name: (r.currentApprover.name || r.currentApprover.email || '—'),
|
||||
avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()),
|
||||
sla: r.currentApprover.sla // ← Backend-calculated SLA
|
||||
} : undefined,
|
||||
createdAt: createdAt || '—',
|
||||
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
|
||||
department: r.department,
|
||||
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
|
||||
};
|
||||
});
|
||||
setItems(mapped);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fetchRequests();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
|
||||
const data = Array.isArray((result as any)?.data)
|
||||
? (result as any).data
|
||||
: Array.isArray((result as any)?.data?.data)
|
||||
? (result as any).data.data
|
||||
: 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;
|
||||
|
||||
return {
|
||||
id: r.requestNumber || r.request_number || r.requestId,
|
||||
requestId: r.requestId,
|
||||
displayId: r.requestNumber || r.request_number || r.requestId,
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending',
|
||||
priority: (r.priority || '').toString().toLowerCase(),
|
||||
initiator: {
|
||||
name: (r.initiator?.displayName || r.initiator?.email || '—'),
|
||||
avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase())
|
||||
},
|
||||
currentApprover: r.currentApprover ? {
|
||||
name: (r.currentApprover.name || r.currentApprover.email || '—'),
|
||||
avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()),
|
||||
sla: r.currentApprover.sla // ← Backend-calculated SLA
|
||||
} : undefined,
|
||||
createdAt: createdAt || '—',
|
||||
approvalStep: r.currentLevel ? `Step ${r.currentLevel} of ${r.totalLevels || '?'}` : undefined,
|
||||
department: r.department,
|
||||
currentLevelSLA: r.currentLevelSLA, // ← Backend-calculated current level SLA
|
||||
};
|
||||
});
|
||||
setItems(mapped);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
<p className="text-xs text-gray-600">{step.role}</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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user