Compare commits
No commits in common. "9b3194d9ca207537c78bc3689f00c066f62d59a7" and "001d636e6c6c73a39ece127ad831b2dbf685a2c0" have entirely different histories.
9b3194d9ca
...
001d636e6c
@ -1,234 +0,0 @@
|
|||||||
# Complete Modular Architecture - Self-Contained Flows
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This architecture ensures that **each flow folder is completely self-contained**. Deleting a flow folder (e.g., `flows/dealer-claim/` or `flows/custom/`) removes **ALL** related code for that flow type. No dependencies remain outside the flow folder.
|
|
||||||
|
|
||||||
## Architecture Principles
|
|
||||||
|
|
||||||
1. **Complete Self-Containment**: Each flow folder contains ALL its related code
|
|
||||||
2. **Zero External Dependencies**: Flow folders don't depend on each other
|
|
||||||
3. **Single Point of Entry**: Main RequestDetail routes to flow-specific screens
|
|
||||||
4. **True Modularity**: Delete a folder = Remove all related functionality
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
flows/
|
|
||||||
├── custom/ # Custom Request Flow (COMPLETE)
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/
|
|
||||||
│ │ │ ├── OverviewTab.tsx # Custom overview
|
|
||||||
│ │ │ └── WorkflowTab.tsx # Custom workflow
|
|
||||||
│ │ └── request-creation/
|
|
||||||
│ │ └── CreateRequest.tsx # Custom creation
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # COMPLETE Custom RequestDetail screen
|
|
||||||
│ ├── hooks/ # Custom-specific hooks (future)
|
|
||||||
│ ├── services/ # Custom-specific services (future)
|
|
||||||
│ ├── utils/ # Custom-specific utilities (future)
|
|
||||||
│ ├── types/ # Custom-specific types (future)
|
|
||||||
│ └── index.ts # Exports all Custom components
|
|
||||||
│
|
|
||||||
├── dealer-claim/ # Dealer Claim Flow (COMPLETE)
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/
|
|
||||||
│ │ │ ├── OverviewTab.tsx # Dealer claim overview
|
|
||||||
│ │ │ ├── WorkflowTab.tsx # Dealer claim workflow
|
|
||||||
│ │ │ ├── IOTab.tsx # IO management
|
|
||||||
│ │ │ ├── claim-cards/ # All dealer claim cards
|
|
||||||
│ │ │ └── modals/ # All dealer claim modals
|
|
||||||
│ │ └── request-creation/
|
|
||||||
│ │ └── ClaimManagementWizard.tsx
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # COMPLETE Dealer Claim RequestDetail screen
|
|
||||||
│ ├── hooks/ # Dealer claim hooks (future)
|
|
||||||
│ ├── services/ # Dealer claim services (future)
|
|
||||||
│ ├── utils/ # Dealer claim utilities (future)
|
|
||||||
│ ├── types/ # Dealer claim types (future)
|
|
||||||
│ └── index.ts # Exports all Dealer Claim components
|
|
||||||
│
|
|
||||||
├── shared/ # Shared Components (Flow-Agnostic)
|
|
||||||
│ └── components/
|
|
||||||
│ └── request-detail/
|
|
||||||
│ ├── DocumentsTab.tsx # Used by all flows
|
|
||||||
│ ├── ActivityTab.tsx # Used by all flows
|
|
||||||
│ ├── WorkNotesTab.tsx # Used by all flows
|
|
||||||
│ ├── SummaryTab.tsx # Used by all flows
|
|
||||||
│ ├── RequestDetailHeader.tsx
|
|
||||||
│ ├── QuickActionsSidebar.tsx
|
|
||||||
│ └── RequestDetailModals.tsx
|
|
||||||
│
|
|
||||||
└── index.ts # Flow registry and routing utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### Main RequestDetail Router
|
|
||||||
|
|
||||||
The main `pages/RequestDetail/RequestDetail.tsx` is now a **simple router**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. Fetches request to determine flow type
|
|
||||||
const flowType = getRequestFlowType(apiRequest);
|
|
||||||
|
|
||||||
// 2. Gets the appropriate RequestDetail screen from flow registry
|
|
||||||
const RequestDetailScreen = getRequestDetailScreen(flowType);
|
|
||||||
|
|
||||||
// 3. Renders the flow-specific screen
|
|
||||||
return <RequestDetailScreen {...props} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flow-Specific RequestDetail Screens
|
|
||||||
|
|
||||||
Each flow has its **own complete RequestDetail screen**:
|
|
||||||
|
|
||||||
- `flows/custom/pages/RequestDetail.tsx` - Complete custom request detail
|
|
||||||
- `flows/dealer-claim/pages/RequestDetail.tsx` - Complete dealer claim detail
|
|
||||||
|
|
||||||
Each screen:
|
|
||||||
- Uses its own flow-specific components
|
|
||||||
- Uses shared components from `flows/shared/`
|
|
||||||
- Is completely self-contained
|
|
||||||
- Can be deleted without affecting other flows
|
|
||||||
|
|
||||||
## Deleting a Flow Type
|
|
||||||
|
|
||||||
To completely remove a flow type (e.g., Dealer Claim):
|
|
||||||
|
|
||||||
### Step 1: Delete the Flow Folder
|
|
||||||
```bash
|
|
||||||
# Delete the entire folder
|
|
||||||
rm -rf src/flows/dealer-claim/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Update Flow Registry
|
|
||||||
```typescript
|
|
||||||
// src/flows/index.ts
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
// DEALER_CLAIM removed - all code is gone!
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
// case 'DEALER_CLAIM': removed
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Update Type Definitions (Optional)
|
|
||||||
```typescript
|
|
||||||
// src/utils/requestTypeUtils.ts
|
|
||||||
export type RequestFlowType = 'CUSTOM'; // 'DEALER_CLAIM' removed
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** All dealer claim related code is gone:
|
|
||||||
- ✅ RequestDetail screen deleted
|
|
||||||
- ✅ All components deleted
|
|
||||||
- ✅ All modals deleted
|
|
||||||
- ✅ All cards deleted
|
|
||||||
- ✅ All creation wizards deleted
|
|
||||||
- ✅ No orphaned code remains
|
|
||||||
|
|
||||||
## What's Inside Each Flow Folder
|
|
||||||
|
|
||||||
### Custom Flow (`flows/custom/`)
|
|
||||||
- ✅ Request Detail Screen (`pages/RequestDetail.tsx`)
|
|
||||||
- ✅ Request Detail Components (OverviewTab, WorkflowTab)
|
|
||||||
- ✅ Request Creation Component (CreateRequest)
|
|
||||||
- 🔜 Custom-specific hooks
|
|
||||||
- 🔜 Custom-specific services
|
|
||||||
- 🔜 Custom-specific utilities
|
|
||||||
- 🔜 Custom-specific types
|
|
||||||
|
|
||||||
### Dealer Claim Flow (`flows/dealer-claim/`)
|
|
||||||
- ✅ Request Detail Screen (`pages/RequestDetail.tsx`)
|
|
||||||
- ✅ Request Detail Components (OverviewTab, WorkflowTab, IOTab)
|
|
||||||
- ✅ Request Detail Cards (5 cards)
|
|
||||||
- ✅ Request Detail Modals (7 modals)
|
|
||||||
- ✅ Request Creation Component (ClaimManagementWizard)
|
|
||||||
- 🔜 Dealer claim-specific hooks
|
|
||||||
- 🔜 Dealer claim-specific services
|
|
||||||
- 🔜 Dealer claim-specific utilities
|
|
||||||
- 🔜 Dealer claim-specific types
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **True Modularity**: Delete folder = Remove all functionality
|
|
||||||
2. **No Orphaned Code**: All related code is in one place
|
|
||||||
3. **Easy Maintenance**: Find everything for a flow in its folder
|
|
||||||
4. **Independent Development**: Work on flows without affecting others
|
|
||||||
5. **Clear Boundaries**: Know exactly what belongs to which flow
|
|
||||||
6. **Simple Removal**: Remove a flow type in 2 steps
|
|
||||||
|
|
||||||
## File Organization Rules
|
|
||||||
|
|
||||||
### ✅ Flow-Specific → Flow Folder
|
|
||||||
- RequestDetail screen → `flows/{flow}/pages/RequestDetail.tsx`
|
|
||||||
- Request detail components → `flows/{flow}/components/request-detail/`
|
|
||||||
- Request creation → `flows/{flow}/components/request-creation/`
|
|
||||||
- Flow-specific hooks → `flows/{flow}/hooks/`
|
|
||||||
- Flow-specific services → `flows/{flow}/services/`
|
|
||||||
- Flow-specific utils → `flows/{flow}/utils/`
|
|
||||||
- Flow-specific types → `flows/{flow}/types/`
|
|
||||||
|
|
||||||
### ✅ Shared → Shared Folder
|
|
||||||
- Components used by ALL flows → `flows/shared/components/`
|
|
||||||
|
|
||||||
### ✅ Routing → Main RequestDetail
|
|
||||||
- Flow detection and routing → `pages/RequestDetail/RequestDetail.tsx`
|
|
||||||
|
|
||||||
## Example: Adding a New Flow Type
|
|
||||||
|
|
||||||
1. **Create folder structure**:
|
|
||||||
```
|
|
||||||
flows/vendor-payment/
|
|
||||||
├── components/
|
|
||||||
│ ├── request-detail/
|
|
||||||
│ └── request-creation/
|
|
||||||
├── pages/
|
|
||||||
│ └── RequestDetail.tsx # Complete screen
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update registry** (`flows/index.ts`):
|
|
||||||
```typescript
|
|
||||||
import * as VendorPaymentFlow from './vendor-payment';
|
|
||||||
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
VENDOR_PAYMENT: VendorPaymentFlow,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **That's it!** The flow is now plug-and-play.
|
|
||||||
|
|
||||||
## Example: Removing a Flow Type
|
|
||||||
|
|
||||||
1. **Delete folder**: `rm -rf flows/dealer-claim/`
|
|
||||||
2. **Update registry**: Remove from `FlowRegistry` and `getRequestDetailScreen()`
|
|
||||||
3. **Done!** All dealer claim code is removed.
|
|
||||||
|
|
||||||
## Current Status
|
|
||||||
|
|
||||||
✅ **Completed**:
|
|
||||||
- Custom flow folder with RequestDetail screen
|
|
||||||
- Dealer claim flow folder with RequestDetail screen
|
|
||||||
- Main RequestDetail router
|
|
||||||
- Flow registry with routing
|
|
||||||
- Shared components folder
|
|
||||||
|
|
||||||
🔜 **Future Enhancements**:
|
|
||||||
- Move flow-specific hooks to flow folders
|
|
||||||
- Move flow-specific services to flow folders
|
|
||||||
- Move flow-specific utilities to flow folders
|
|
||||||
- Move flow-specific types to flow folders
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The architecture is now **truly modular and self-contained**. Each flow folder is a complete, independent module. Deleting a folder removes all related code with zero dependencies remaining. This makes the codebase maintainable, scalable, and easy to understand.
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
# Flow Deletion Guide - Complete Removal
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This guide explains how to completely remove a flow type from the application. The architecture ensures that **deleting a flow folder removes ALL related code** with zero dependencies remaining.
|
|
||||||
|
|
||||||
## Architecture Guarantee
|
|
||||||
|
|
||||||
✅ **Each flow folder is completely self-contained**
|
|
||||||
- All components, screens, hooks, services, utils, types are in the flow folder
|
|
||||||
- No dependencies on the flow folder from outside (except the registry)
|
|
||||||
- Deleting a folder = Removing all related functionality
|
|
||||||
|
|
||||||
## How to Delete a Flow Type
|
|
||||||
|
|
||||||
### Example: Removing Dealer Claim Flow
|
|
||||||
|
|
||||||
#### Step 1: Delete the Flow Folder
|
|
||||||
```bash
|
|
||||||
# Delete the entire dealer-claim folder
|
|
||||||
rm -rf src/flows/dealer-claim/
|
|
||||||
```
|
|
||||||
|
|
||||||
**What gets deleted:**
|
|
||||||
- ✅ `pages/RequestDetail.tsx` - Complete dealer claim detail screen
|
|
||||||
- ✅ All request detail components (OverviewTab, WorkflowTab, IOTab)
|
|
||||||
- ✅ All claim cards (5 cards)
|
|
||||||
- ✅ All modals (7 modals)
|
|
||||||
- ✅ Request creation wizard
|
|
||||||
- ✅ All future hooks, services, utils, types
|
|
||||||
|
|
||||||
#### Step 2: Update Flow Registry
|
|
||||||
```typescript
|
|
||||||
// src/flows/index.ts
|
|
||||||
|
|
||||||
// Remove import
|
|
||||||
// import * as DealerClaimFlow from './dealer-claim';
|
|
||||||
|
|
||||||
// Update FlowRegistry
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
// DEALER_CLAIM: DealerClaimFlow, // REMOVED
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Update getRequestDetailScreen()
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
// case 'DEALER_CLAIM': // REMOVED
|
|
||||||
// return DealerClaimFlow.DealerClaimRequestDetail;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update other functions similarly
|
|
||||||
export function getOverviewTab(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
// case 'DEALER_CLAIM': // REMOVED
|
|
||||||
// return DealerClaimFlow.DealerClaimOverviewTab;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomOverviewTab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 3: Update Type Definitions (Optional)
|
|
||||||
```typescript
|
|
||||||
// src/utils/requestTypeUtils.ts
|
|
||||||
|
|
||||||
// Remove from type union
|
|
||||||
export type RequestFlowType = 'CUSTOM'; // 'DEALER_CLAIM' removed
|
|
||||||
|
|
||||||
// Remove detection function (optional - can keep for backward compatibility)
|
|
||||||
// export function isDealerClaimRequest(request: any): boolean { ... }
|
|
||||||
|
|
||||||
// Update getRequestFlowType()
|
|
||||||
export function getRequestFlowType(request: any): RequestFlowType {
|
|
||||||
// if (isDealerClaimRequest(request)) return 'DEALER_CLAIM'; // REMOVED
|
|
||||||
return 'CUSTOM';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 4: Update Navigation (If Needed)
|
|
||||||
```typescript
|
|
||||||
// src/utils/requestNavigation.ts
|
|
||||||
|
|
||||||
export function navigateToCreateRequest(
|
|
||||||
navigate: NavigateFunction,
|
|
||||||
flowType: RequestFlowType = 'CUSTOM'
|
|
||||||
): void {
|
|
||||||
// Remove dealer claim case
|
|
||||||
// if (flowType === 'DEALER_CLAIM') {
|
|
||||||
// return '/claim-management';
|
|
||||||
// }
|
|
||||||
return '/new-request';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Step 5: Remove Routes (If Needed)
|
|
||||||
```typescript
|
|
||||||
// src/App.tsx
|
|
||||||
|
|
||||||
// Remove dealer claim route
|
|
||||||
// <Route
|
|
||||||
// path="/claim-management"
|
|
||||||
// element={<ClaimManagementWizard ... />}
|
|
||||||
// />
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** All dealer claim code is completely removed.
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
After deleting a flow, verify:
|
|
||||||
|
|
||||||
- [ ] Flow folder deleted
|
|
||||||
- [ ] FlowRegistry updated
|
|
||||||
- [ ] All `get*()` functions updated
|
|
||||||
- [ ] Type definitions updated (optional)
|
|
||||||
- [ ] Navigation updated (if needed)
|
|
||||||
- [ ] Routes removed (if needed)
|
|
||||||
- [ ] No broken imports
|
|
||||||
- [ ] Application compiles successfully
|
|
||||||
- [ ] No references to deleted flow in codebase
|
|
||||||
|
|
||||||
## What Happens When You Delete a Flow
|
|
||||||
|
|
||||||
### ✅ Removed
|
|
||||||
- Complete RequestDetail screen
|
|
||||||
- All flow-specific components
|
|
||||||
- All flow-specific modals
|
|
||||||
- All flow-specific cards
|
|
||||||
- Request creation wizard
|
|
||||||
- All flow-specific code
|
|
||||||
|
|
||||||
### ✅ Still Works
|
|
||||||
- Other flow types continue working
|
|
||||||
- Shared components remain
|
|
||||||
- Main RequestDetail router handles remaining flows
|
|
||||||
- Navigation for remaining flows
|
|
||||||
|
|
||||||
### ✅ No Orphaned Code
|
|
||||||
- No broken imports
|
|
||||||
- No dangling references
|
|
||||||
- No unused components
|
|
||||||
- Clean removal
|
|
||||||
|
|
||||||
## Current Flow Structure
|
|
||||||
|
|
||||||
### Custom Flow (`flows/custom/`)
|
|
||||||
**Contains:**
|
|
||||||
- `pages/RequestDetail.tsx` - Complete custom request detail screen
|
|
||||||
- `components/request-detail/` - Custom detail components
|
|
||||||
- `components/request-creation/` - Custom creation component
|
|
||||||
|
|
||||||
**To remove:** Delete `flows/custom/` folder and update registry
|
|
||||||
|
|
||||||
### Dealer Claim Flow (`flows/dealer-claim/`)
|
|
||||||
**Contains:**
|
|
||||||
- `pages/RequestDetail.tsx` - Complete dealer claim detail screen
|
|
||||||
- `components/request-detail/` - Dealer claim detail components
|
|
||||||
- `components/request-detail/claim-cards/` - 5 claim cards
|
|
||||||
- `components/request-detail/modals/` - 7 modals
|
|
||||||
- `components/request-creation/` - Claim management wizard
|
|
||||||
|
|
||||||
**To remove:** Delete `flows/dealer-claim/` folder and update registry
|
|
||||||
|
|
||||||
## Benefits of This Architecture
|
|
||||||
|
|
||||||
1. **True Modularity**: Each flow is independent
|
|
||||||
2. **Easy Removal**: Delete folder + update registry = Done
|
|
||||||
3. **No Side Effects**: Removing one flow doesn't affect others
|
|
||||||
4. **Clear Ownership**: Know exactly what belongs to which flow
|
|
||||||
5. **Maintainable**: All related code in one place
|
|
||||||
6. **Scalable**: Easy to add new flows
|
|
||||||
|
|
||||||
## Example: Complete Removal
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Delete folder
|
|
||||||
rm -rf src/flows/dealer-claim/
|
|
||||||
|
|
||||||
# 2. Update registry (remove 3 lines)
|
|
||||||
# 3. Update type (remove 1 line)
|
|
||||||
# 4. Done! All dealer claim code is gone.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Time to remove a flow:** ~2 minutes
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The architecture ensures that **deleting a flow folder removes ALL related code**. There are no dependencies, no orphaned files, and no cleanup needed. Each flow is a complete, self-contained module that can be added or removed independently.
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
# Complete Flow Segregation - Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document describes the **complete segregation** of request flows into dedicated folders. Each flow type (CUSTOM, DEALER_CLAIM) now has ALL its related components, hooks, services, utilities, and types in its own folder. Only truly shared components remain in the `shared/` folder.
|
|
||||||
|
|
||||||
## What Was Done
|
|
||||||
|
|
||||||
### 1. Created Complete Folder Structure
|
|
||||||
|
|
||||||
#### Custom Flow (`src/flows/custom/`)
|
|
||||||
```
|
|
||||||
custom/
|
|
||||||
├── components/
|
|
||||||
│ ├── request-detail/
|
|
||||||
│ │ ├── OverviewTab.tsx # Custom request overview
|
|
||||||
│ │ └── WorkflowTab.tsx # Custom request workflow
|
|
||||||
│ └── request-creation/
|
|
||||||
│ └── CreateRequest.tsx # Custom request creation
|
|
||||||
└── index.ts # Exports all custom components
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Dealer Claim Flow (`src/flows/dealer-claim/`)
|
|
||||||
```
|
|
||||||
dealer-claim/
|
|
||||||
├── components/
|
|
||||||
│ ├── request-detail/
|
|
||||||
│ │ ├── OverviewTab.tsx # Dealer claim overview
|
|
||||||
│ │ ├── WorkflowTab.tsx # Dealer claim workflow
|
|
||||||
│ │ ├── IOTab.tsx # IO management (dealer claim specific)
|
|
||||||
│ │ ├── claim-cards/ # All dealer claim cards
|
|
||||||
│ │ │ ├── ActivityInformationCard.tsx
|
|
||||||
│ │ │ ├── DealerInformationCard.tsx
|
|
||||||
│ │ │ ├── ProcessDetailsCard.tsx
|
|
||||||
│ │ │ ├── ProposalDetailsCard.tsx
|
|
||||||
│ │ │ └── RequestInitiatorCard.tsx
|
|
||||||
│ │ └── modals/ # All dealer claim modals
|
|
||||||
│ │ ├── CreditNoteSAPModal.tsx
|
|
||||||
│ │ ├── DealerCompletionDocumentsModal.tsx
|
|
||||||
│ │ ├── DealerProposalSubmissionModal.tsx
|
|
||||||
│ │ ├── DeptLeadIOApprovalModal.tsx
|
|
||||||
│ │ ├── EditClaimAmountModal.tsx
|
|
||||||
│ │ ├── EmailNotificationTemplateModal.tsx
|
|
||||||
│ │ └── InitiatorProposalApprovalModal.tsx
|
|
||||||
│ └── request-creation/
|
|
||||||
│ └── ClaimManagementWizard.tsx # Dealer claim creation
|
|
||||||
└── index.ts # Exports all dealer claim components
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Shared Components (`src/flows/shared/`)
|
|
||||||
```
|
|
||||||
shared/
|
|
||||||
└── components/
|
|
||||||
└── request-detail/
|
|
||||||
├── DocumentsTab.tsx # Used by all flows
|
|
||||||
├── ActivityTab.tsx # Used by all flows
|
|
||||||
├── WorkNotesTab.tsx # Used by all flows
|
|
||||||
├── SummaryTab.tsx # Used by all flows
|
|
||||||
├── RequestDetailHeader.tsx # Used by all flows
|
|
||||||
├── QuickActionsSidebar.tsx # Used by all flows
|
|
||||||
└── RequestDetailModals.tsx # Used by all flows
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Updated Flow Registry
|
|
||||||
|
|
||||||
The flow registry (`src/flows/index.ts`) now:
|
|
||||||
- Exports all flow modules
|
|
||||||
- Provides utility functions to get flow-specific components
|
|
||||||
- Includes `getCreateRequestComponent()` for request creation
|
|
||||||
- Exports `SharedComponents` for shared components
|
|
||||||
|
|
||||||
### 3. Updated RequestDetail Component
|
|
||||||
|
|
||||||
The `RequestDetail` component now:
|
|
||||||
- Uses flow registry to get flow-specific components
|
|
||||||
- Imports shared components from `SharedComponents`
|
|
||||||
- Dynamically loads appropriate tabs based on flow type
|
|
||||||
- Maintains backward compatibility
|
|
||||||
|
|
||||||
## File Organization Rules
|
|
||||||
|
|
||||||
### ✅ Flow-Specific Files → Flow Folders
|
|
||||||
|
|
||||||
**Custom Flow:**
|
|
||||||
- Custom request creation wizard
|
|
||||||
- Custom request detail tabs (Overview, Workflow)
|
|
||||||
- Custom request hooks (future)
|
|
||||||
- Custom request services (future)
|
|
||||||
- Custom request utilities (future)
|
|
||||||
- Custom request types (future)
|
|
||||||
|
|
||||||
**Dealer Claim Flow:**
|
|
||||||
- Dealer claim creation wizard
|
|
||||||
- Dealer claim detail tabs (Overview, Workflow, IO)
|
|
||||||
- Dealer claim cards (Activity, Dealer, Process, Proposal, Initiator)
|
|
||||||
- Dealer claim modals (all 7 modals)
|
|
||||||
- Dealer claim hooks (future)
|
|
||||||
- Dealer claim services (future)
|
|
||||||
- Dealer claim utilities (future)
|
|
||||||
- Dealer claim types (future)
|
|
||||||
|
|
||||||
### ✅ Shared Files → Shared Folder
|
|
||||||
|
|
||||||
**Shared Components:**
|
|
||||||
- DocumentsTab (used by all flows)
|
|
||||||
- ActivityTab (used by all flows)
|
|
||||||
- WorkNotesTab (used by all flows)
|
|
||||||
- SummaryTab (used by all flows)
|
|
||||||
- RequestDetailHeader (used by all flows)
|
|
||||||
- QuickActionsSidebar (used by all flows)
|
|
||||||
- RequestDetailModals (used by all flows)
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Getting Flow-Specific Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { getOverviewTab, getWorkflowTab, getCreateRequestComponent } from '@/flows';
|
|
||||||
import { getRequestFlowType } from '@/utils/requestTypeUtils';
|
|
||||||
|
|
||||||
const flowType = getRequestFlowType(request);
|
|
||||||
const OverviewTab = getOverviewTab(flowType);
|
|
||||||
const WorkflowTab = getWorkflowTab(flowType);
|
|
||||||
const CreateRequest = getCreateRequestComponent(flowType);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Shared Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SharedComponents } from '@/flows';
|
|
||||||
|
|
||||||
const { DocumentsTab, ActivityTab, WorkNotesTab, SummaryTab } = SharedComponents;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct Access to Flow Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { CustomFlow, DealerClaimFlow } from '@/flows';
|
|
||||||
|
|
||||||
// Custom flow
|
|
||||||
<CustomFlow.CustomOverviewTab {...props} />
|
|
||||||
<CustomFlow.CustomCreateRequest {...props} />
|
|
||||||
|
|
||||||
// Dealer claim flow
|
|
||||||
<DealerClaimFlow.DealerClaimOverviewTab {...props} />
|
|
||||||
<DealerClaimFlow.IOTab {...props} />
|
|
||||||
<DealerClaimFlow.ClaimManagementWizard {...props} />
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Complete Segregation**: Each flow is completely isolated
|
|
||||||
2. **Easy Navigation**: All files for a flow type are in one place
|
|
||||||
3. **Maintainability**: Changes to one flow don't affect others
|
|
||||||
4. **Scalability**: Easy to add new flow types
|
|
||||||
5. **Clarity**: Clear separation between flow-specific and shared code
|
|
||||||
6. **Type Safety**: TypeScript ensures correct usage
|
|
||||||
|
|
||||||
## Next Steps (Future Enhancements)
|
|
||||||
|
|
||||||
1. **Move Flow-Specific Hooks**
|
|
||||||
- Custom hooks → `flows/custom/hooks/`
|
|
||||||
- Dealer claim hooks → `flows/dealer-claim/hooks/`
|
|
||||||
|
|
||||||
2. **Move Flow-Specific Services**
|
|
||||||
- Custom services → `flows/custom/services/`
|
|
||||||
- Dealer claim services → `flows/dealer-claim/services/`
|
|
||||||
|
|
||||||
3. **Move Flow-Specific Utilities**
|
|
||||||
- Custom utilities → `flows/custom/utils/`
|
|
||||||
- Dealer claim utilities → `flows/dealer-claim/utils/`
|
|
||||||
|
|
||||||
4. **Move Flow-Specific Types**
|
|
||||||
- Custom types → `flows/custom/types/`
|
|
||||||
- Dealer claim types → `flows/dealer-claim/types/`
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
### Custom Flow
|
|
||||||
- `src/flows/custom/components/request-detail/OverviewTab.tsx`
|
|
||||||
- `src/flows/custom/components/request-detail/WorkflowTab.tsx`
|
|
||||||
- `src/flows/custom/components/request-creation/CreateRequest.tsx`
|
|
||||||
- `src/flows/custom/index.ts` (updated)
|
|
||||||
|
|
||||||
### Dealer Claim Flow
|
|
||||||
- `src/flows/dealer-claim/components/request-detail/OverviewTab.tsx`
|
|
||||||
- `src/flows/dealer-claim/components/request-detail/WorkflowTab.tsx`
|
|
||||||
- `src/flows/dealer-claim/components/request-detail/IOTab.tsx`
|
|
||||||
- `src/flows/dealer-claim/components/request-detail/claim-cards/index.ts`
|
|
||||||
- `src/flows/dealer-claim/components/request-detail/modals/index.ts`
|
|
||||||
- `src/flows/dealer-claim/components/request-creation/ClaimManagementWizard.tsx`
|
|
||||||
- `src/flows/dealer-claim/index.ts` (updated)
|
|
||||||
|
|
||||||
### Shared Components
|
|
||||||
- `src/flows/shared/components/request-detail/DocumentsTab.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/ActivityTab.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/WorkNotesTab.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/SummaryTab.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/RequestDetailHeader.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/QuickActionsSidebar.tsx`
|
|
||||||
- `src/flows/shared/components/request-detail/RequestDetailModals.tsx`
|
|
||||||
- `src/flows/shared/components/index.ts` (updated)
|
|
||||||
|
|
||||||
### Registry
|
|
||||||
- `src/flows/index.ts` (updated with new structure)
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
- `src/pages/RequestDetail/RequestDetail.tsx` - Uses new flow structure
|
|
||||||
- `src/flows/README.md` - Updated with complete segregation documentation
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The complete segregation is now in place. Each flow type has its own dedicated folder with all related components. This makes it easy to:
|
|
||||||
- Find all files related to a specific flow type
|
|
||||||
- Maintain and update flow-specific code
|
|
||||||
- Add new flow types without affecting existing ones
|
|
||||||
- Understand what is shared vs. flow-specific
|
|
||||||
|
|
||||||
The architecture is now truly modular and plug-and-play!
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
# Flow Structure at Source Level - Complete Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Flow folders are now at the **`src/` level** for maximum visibility and easy removal. This makes it immediately clear what flows exist and makes deletion trivial.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── custom/ # ✅ Custom Request Flow
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/ # Custom detail components
|
|
||||||
│ │ └── request-creation/ # Custom creation component
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # Complete custom request detail screen
|
|
||||||
│ └── index.ts # Exports all custom components
|
|
||||||
│
|
|
||||||
├── dealer-claim/ # ✅ Dealer Claim Flow
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/ # Dealer claim detail components
|
|
||||||
│ │ │ ├── claim-cards/ # 5 claim cards
|
|
||||||
│ │ │ └── modals/ # 7 modals
|
|
||||||
│ │ └── request-creation/ # Claim management wizard
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # Complete dealer claim detail screen
|
|
||||||
│ └── index.ts # Exports all dealer claim components
|
|
||||||
│
|
|
||||||
├── shared/ # ✅ Shared Components
|
|
||||||
│ └── components/
|
|
||||||
│ └── request-detail/ # Components used by all flows
|
|
||||||
│
|
|
||||||
└── flows.ts # ✅ Flow registry and routing
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Benefits
|
|
||||||
|
|
||||||
### 1. Maximum Visibility
|
|
||||||
- Flow folders are directly visible at `src/` level
|
|
||||||
- No nested paths to navigate
|
|
||||||
- Clear separation from other code
|
|
||||||
|
|
||||||
### 2. Easy Removal
|
|
||||||
- Delete `src/custom/` → All custom code gone
|
|
||||||
- Delete `src/dealer-claim/` → All dealer claim code gone
|
|
||||||
- Update `src/flows.ts` → Done!
|
|
||||||
|
|
||||||
### 3. Complete Self-Containment
|
|
||||||
- Each flow folder contains ALL its code
|
|
||||||
- No dependencies outside the folder (except registry)
|
|
||||||
- Future hooks, services, utils, types go in flow folders
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### Importing Flow Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// From flow registry
|
|
||||||
import { getRequestDetailScreen, CustomFlow, DealerClaimFlow } from '@/flows';
|
|
||||||
|
|
||||||
// Direct from flow folders
|
|
||||||
import { CustomRequestDetail } from '@/custom';
|
|
||||||
import { DealerClaimRequestDetail } from '@/dealer-claim';
|
|
||||||
|
|
||||||
// Shared components
|
|
||||||
import { SharedComponents } from '@/shared/components';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Main RequestDetail Router
|
|
||||||
|
|
||||||
The main `src/pages/RequestDetail/RequestDetail.tsx` routes to flow-specific screens:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const flowType = getRequestFlowType(apiRequest);
|
|
||||||
const RequestDetailScreen = getRequestDetailScreen(flowType);
|
|
||||||
return <RequestDetailScreen {...props} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deleting a Flow
|
|
||||||
|
|
||||||
### Step 1: Delete Folder
|
|
||||||
```bash
|
|
||||||
# Delete entire flow folder
|
|
||||||
rm -rf src/dealer-claim/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Update Registry
|
|
||||||
```typescript
|
|
||||||
// src/flows.ts
|
|
||||||
// Remove: import * as DealerClaimFlow from './dealer-claim';
|
|
||||||
// Remove: DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
// Update: getRequestDetailScreen() to remove dealer claim case
|
|
||||||
```
|
|
||||||
|
|
||||||
**That's it!** All dealer claim code is completely removed.
|
|
||||||
|
|
||||||
## File Locations
|
|
||||||
|
|
||||||
### Custom Flow (`src/custom/`)
|
|
||||||
- `pages/RequestDetail.tsx` - Complete custom request detail screen
|
|
||||||
- `components/request-detail/OverviewTab.tsx`
|
|
||||||
- `components/request-detail/WorkflowTab.tsx`
|
|
||||||
- `components/request-creation/CreateRequest.tsx`
|
|
||||||
- `index.ts` - Exports all custom components
|
|
||||||
|
|
||||||
### Dealer Claim Flow (`src/dealer-claim/`)
|
|
||||||
- `pages/RequestDetail.tsx` - Complete dealer claim detail screen
|
|
||||||
- `components/request-detail/OverviewTab.tsx`
|
|
||||||
- `components/request-detail/WorkflowTab.tsx`
|
|
||||||
- `components/request-detail/IOTab.tsx`
|
|
||||||
- `components/request-detail/claim-cards/` - 5 cards
|
|
||||||
- `components/request-detail/modals/` - 7 modals
|
|
||||||
- `components/request-creation/ClaimManagementWizard.tsx`
|
|
||||||
- `index.ts` - Exports all dealer claim components
|
|
||||||
|
|
||||||
### Shared Components (`src/shared/`)
|
|
||||||
- `components/request-detail/DocumentsTab.tsx`
|
|
||||||
- `components/request-detail/ActivityTab.tsx`
|
|
||||||
- `components/request-detail/WorkNotesTab.tsx`
|
|
||||||
- `components/request-detail/SummaryTab.tsx`
|
|
||||||
- `components/request-detail/RequestDetailHeader.tsx`
|
|
||||||
- `components/request-detail/QuickActionsSidebar.tsx`
|
|
||||||
- `components/request-detail/RequestDetailModals.tsx`
|
|
||||||
- `components/index.ts` - Exports all shared components
|
|
||||||
|
|
||||||
### Flow Registry (`src/flows.ts`)
|
|
||||||
- FlowRegistry mapping
|
|
||||||
- `getRequestDetailScreen()` - Routes to flow-specific screens
|
|
||||||
- `getOverviewTab()` - Gets flow-specific overview tabs
|
|
||||||
- `getWorkflowTab()` - Gets flow-specific workflow tabs
|
|
||||||
- `getCreateRequestComponent()` - Gets flow-specific creation components
|
|
||||||
|
|
||||||
## Import Examples
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Flow registry
|
|
||||||
import { getRequestDetailScreen } from '@/flows';
|
|
||||||
|
|
||||||
// Direct flow imports
|
|
||||||
import { CustomRequestDetail } from '@/custom';
|
|
||||||
import { DealerClaimRequestDetail } from '@/dealer-claim';
|
|
||||||
|
|
||||||
// Shared components
|
|
||||||
import { SharedComponents } from '@/shared/components';
|
|
||||||
const { DocumentsTab, ActivityTab } = SharedComponents;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding a New Flow
|
|
||||||
|
|
||||||
1. **Create folder**: `src/vendor-payment/`
|
|
||||||
2. **Create structure**:
|
|
||||||
```
|
|
||||||
src/vendor-payment/
|
|
||||||
├── components/
|
|
||||||
│ ├── request-detail/
|
|
||||||
│ └── request-creation/
|
|
||||||
├── pages/
|
|
||||||
│ └── RequestDetail.tsx
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
3. **Update `src/flows.ts`**:
|
|
||||||
```typescript
|
|
||||||
import * as VendorPaymentFlow from './vendor-payment';
|
|
||||||
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
VENDOR_PAYMENT: VendorPaymentFlow,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The architecture is now **completely modular at the source level**. Flow folders are directly under `src/` for maximum visibility, easy navigation, and trivial removal. Each flow is a complete, self-contained module.
|
|
||||||
@ -1,257 +0,0 @@
|
|||||||
# Modular Request Flow Architecture - Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document summarizes the implementation of a modular, plug-and-play architecture for handling different request flow types (CUSTOM and DEALER_CLAIM) in the application.
|
|
||||||
|
|
||||||
## What Was Implemented
|
|
||||||
|
|
||||||
### 1. Request Type Detection Utilities (`src/utils/requestTypeUtils.ts`)
|
|
||||||
|
|
||||||
Created centralized utilities for detecting and handling different request types:
|
|
||||||
|
|
||||||
- `isCustomRequest(request)` - Checks if a request is a custom request
|
|
||||||
- `isDealerClaimRequest(request)` - Checks if a request is a dealer claim request
|
|
||||||
- `getRequestFlowType(request)` - Returns the flow type ('CUSTOM' | 'DEALER_CLAIM')
|
|
||||||
- `getRequestDetailRoute(requestId, request?)` - Gets the appropriate route for request detail
|
|
||||||
- `getCreateRequestRoute(flowType)` - Gets the route for creating a new request
|
|
||||||
|
|
||||||
### 2. Global Navigation Utility (`src/utils/requestNavigation.ts`)
|
|
||||||
|
|
||||||
Created a single point of navigation logic for all request-related routes:
|
|
||||||
|
|
||||||
- `navigateToRequest(options)` - Main navigation function that handles:
|
|
||||||
- Draft requests (routes to edit)
|
|
||||||
- Different flow types
|
|
||||||
- Status-based routing
|
|
||||||
- `navigateToCreateRequest(navigate, flowType)` - Navigate to create request based on flow type
|
|
||||||
- `createRequestNavigationHandler(navigate)` - Factory function for creating navigation handlers
|
|
||||||
|
|
||||||
### 3. Modular Flow Structure (`src/flows/`)
|
|
||||||
|
|
||||||
Created a modular folder structure for different request flows:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/flows/
|
|
||||||
├── custom/
|
|
||||||
│ └── index.ts # Exports Custom flow components
|
|
||||||
├── dealer-claim/
|
|
||||||
│ └── index.ts # Exports Dealer Claim flow components
|
|
||||||
├── shared/
|
|
||||||
│ └── components/ # Shared components (for future use)
|
|
||||||
└── index.ts # Flow registry and utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flow Registry** (`src/flows/index.ts`):
|
|
||||||
- `FlowRegistry` - Maps flow types to their modules
|
|
||||||
- `getFlowModule(flowType)` - Gets the flow module for a type
|
|
||||||
- `getOverviewTab(flowType)` - Gets the appropriate overview tab component
|
|
||||||
- `getWorkflowTab(flowType)` - Gets the appropriate workflow tab component
|
|
||||||
|
|
||||||
### 4. Updated RequestDetail Component
|
|
||||||
|
|
||||||
Modified `src/pages/RequestDetail/RequestDetail.tsx` to:
|
|
||||||
- Use flow type detection instead of hardcoded checks
|
|
||||||
- Dynamically load appropriate components based on flow type
|
|
||||||
- Support plug-and-play architecture for different flows
|
|
||||||
|
|
||||||
**Key Changes**:
|
|
||||||
- Replaced `isClaimManagementRequest()` with `getRequestFlowType()`
|
|
||||||
- Uses `getOverviewTab()` and `getWorkflowTab()` to get flow-specific components
|
|
||||||
- Maintains backward compatibility with existing components
|
|
||||||
|
|
||||||
### 5. Updated Navigation Throughout App
|
|
||||||
|
|
||||||
Updated all request card click handlers to use the global navigation utility:
|
|
||||||
|
|
||||||
**Files Updated**:
|
|
||||||
- `src/App.tsx` - Main `handleViewRequest` function
|
|
||||||
- `src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx`
|
|
||||||
- `src/pages/DetailedReports/DetailedReports.tsx`
|
|
||||||
|
|
||||||
All navigation now goes through `navigateToRequest()` for consistency.
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
|
|
||||||
### 1. Navigating to a Request
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { navigateToRequest } from '@/utils/requestNavigation';
|
|
||||||
|
|
||||||
// In a component with navigate function
|
|
||||||
navigateToRequest({
|
|
||||||
requestId: 'REQ-123',
|
|
||||||
requestTitle: 'My Request',
|
|
||||||
status: 'pending',
|
|
||||||
request: requestObject, // Optional: helps determine flow type
|
|
||||||
navigate: navigate,
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Getting Flow-Specific Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { getOverviewTab, getWorkflowTab } from '@/flows';
|
|
||||||
import { getRequestFlowType } from '@/utils/requestTypeUtils';
|
|
||||||
|
|
||||||
const flowType = getRequestFlowType(request);
|
|
||||||
const OverviewTab = getOverviewTab(flowType);
|
|
||||||
const WorkflowTab = getWorkflowTab(flowType);
|
|
||||||
|
|
||||||
// Use in JSX
|
|
||||||
<OverviewTab {...props} />
|
|
||||||
<WorkflowTab {...props} />
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Detecting Request Type
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {
|
|
||||||
getRequestFlowType,
|
|
||||||
isCustomRequest,
|
|
||||||
isDealerClaimRequest
|
|
||||||
} from '@/utils/requestTypeUtils';
|
|
||||||
|
|
||||||
// Check specific type
|
|
||||||
if (isDealerClaimRequest(request)) {
|
|
||||||
// Handle dealer claim specific logic
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get flow type
|
|
||||||
const flowType = getRequestFlowType(request); // 'CUSTOM' | 'DEALER_CLAIM'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding a New Flow Type
|
|
||||||
|
|
||||||
To add a new flow type (e.g., "VENDOR_PAYMENT"):
|
|
||||||
|
|
||||||
### Step 1: Update Type Definitions
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/utils/requestTypeUtils.ts
|
|
||||||
export type RequestFlowType = 'CUSTOM' | 'DEALER_CLAIM' | 'VENDOR_PAYMENT';
|
|
||||||
|
|
||||||
export function isVendorPaymentRequest(request: any): boolean {
|
|
||||||
// Add detection logic
|
|
||||||
return request.workflowType === 'VENDOR_PAYMENT';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRequestFlowType(request: any): RequestFlowType {
|
|
||||||
if (isVendorPaymentRequest(request)) return 'VENDOR_PAYMENT';
|
|
||||||
if (isDealerClaimRequest(request)) return 'DEALER_CLAIM';
|
|
||||||
return 'CUSTOM';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Create Flow Folder
|
|
||||||
|
|
||||||
```
|
|
||||||
src/flows/vendor-payment/
|
|
||||||
└── index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/flows/vendor-payment/index.ts
|
|
||||||
export { VendorPaymentOverviewTab } from '@/pages/RequestDetail/components/tabs/VendorPaymentOverviewTab';
|
|
||||||
export { VendorPaymentWorkflowTab } from '@/pages/RequestDetail/components/tabs/VendorPaymentWorkflowTab';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Update Flow Registry
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/flows/index.ts
|
|
||||||
import * as VendorPaymentFlow from './vendor-payment';
|
|
||||||
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
VENDOR_PAYMENT: VendorPaymentFlow,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function getOverviewTab(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'VENDOR_PAYMENT':
|
|
||||||
return VendorPaymentFlow.VendorPaymentOverviewTab;
|
|
||||||
// ... existing cases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Create Components
|
|
||||||
|
|
||||||
Create the flow-specific components in `src/pages/RequestDetail/components/tabs/`:
|
|
||||||
- `VendorPaymentOverviewTab.tsx`
|
|
||||||
- `VendorPaymentWorkflowTab.tsx`
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Modularity**: Each flow type is isolated in its own folder
|
|
||||||
2. **Maintainability**: Changes to one flow don't affect others
|
|
||||||
3. **Scalability**: Easy to add new flow types
|
|
||||||
4. **Consistency**: Single navigation utility ensures consistent routing
|
|
||||||
5. **Type Safety**: TypeScript ensures correct usage
|
|
||||||
6. **Reusability**: Shared components can be used across flows
|
|
||||||
|
|
||||||
## Migration Notes
|
|
||||||
|
|
||||||
### Backward Compatibility
|
|
||||||
|
|
||||||
- All existing code continues to work
|
|
||||||
- `isClaimManagementRequest()` still works (now uses `isDealerClaimRequest()` internally)
|
|
||||||
- Existing components are preserved and work as before
|
|
||||||
|
|
||||||
### Breaking Changes
|
|
||||||
|
|
||||||
None. This is a non-breaking enhancement.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
To test the new architecture:
|
|
||||||
|
|
||||||
1. **Test Custom Requests**:
|
|
||||||
- Create a custom request
|
|
||||||
- Navigate to its detail page
|
|
||||||
- Verify correct components are loaded
|
|
||||||
|
|
||||||
2. **Test Dealer Claim Requests**:
|
|
||||||
- Create a dealer claim request
|
|
||||||
- Navigate to its detail page
|
|
||||||
- Verify dealer claim-specific components are loaded
|
|
||||||
|
|
||||||
3. **Test Navigation**:
|
|
||||||
- Click request cards from various pages
|
|
||||||
- Verify navigation works correctly
|
|
||||||
- Test draft requests (should route to edit)
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Add flow-specific validation rules
|
|
||||||
- [ ] Add flow-specific API endpoints
|
|
||||||
- [ ] Add flow-specific permissions
|
|
||||||
- [ ] Add flow-specific analytics
|
|
||||||
- [ ] Add flow-specific notifications
|
|
||||||
- [ ] Create shared request card component
|
|
||||||
- [ ] Add flow-specific creation wizards
|
|
||||||
|
|
||||||
## Files Created
|
|
||||||
|
|
||||||
1. `src/utils/requestTypeUtils.ts` - Request type detection utilities
|
|
||||||
2. `src/utils/requestNavigation.ts` - Global navigation utility
|
|
||||||
3. `src/flows/custom/index.ts` - Custom flow exports
|
|
||||||
4. `src/flows/dealer-claim/index.ts` - Dealer claim flow exports
|
|
||||||
5. `src/flows/index.ts` - Flow registry
|
|
||||||
6. `src/flows/shared/components/index.ts` - Shared components placeholder
|
|
||||||
7. `src/flows/README.md` - Flow architecture documentation
|
|
||||||
|
|
||||||
## Files Modified
|
|
||||||
|
|
||||||
1. `src/pages/RequestDetail/RequestDetail.tsx` - Uses flow registry
|
|
||||||
2. `src/App.tsx` - Uses navigation utility
|
|
||||||
3. `src/pages/ApproverPerformance/components/ApproverPerformanceRequestList.tsx` - Uses navigation utility
|
|
||||||
4. `src/pages/DetailedReports/DetailedReports.tsx` - Uses navigation utility
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The modular architecture is now in place and ready for use. The system supports plug-and-play flow types, making it easy to add new request types in the future while maintaining clean separation of concerns.
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
# Request Detail Routing Flow - How It Works
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document explains how `RequestDetail.tsx` routes to flow-specific detail screens based on the request type.
|
|
||||||
|
|
||||||
## Complete Flow Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
User clicks on Request Card
|
|
||||||
↓
|
|
||||||
Navigate to /request/{requestId}
|
|
||||||
↓
|
|
||||||
RequestDetail.tsx (Router Component)
|
|
||||||
↓
|
|
||||||
Step 1: Fetch Request Data
|
|
||||||
├─ useRequestDetails(requestId, dynamicRequests, user)
|
|
||||||
├─ Calls API: workflowApi.getWorkflowDetails(requestId)
|
|
||||||
└─ Returns: apiRequest (full request object)
|
|
||||||
↓
|
|
||||||
Step 2: Determine Flow Type
|
|
||||||
├─ getRequestFlowType(apiRequest)
|
|
||||||
├─ Checks: request.workflowType, request.templateType, etc.
|
|
||||||
└─ Returns: 'CUSTOM' | 'DEALER_CLAIM'
|
|
||||||
↓
|
|
||||||
Step 3: Get Flow-Specific Screen Component
|
|
||||||
├─ getRequestDetailScreen(flowType)
|
|
||||||
├─ From: src/flows.ts
|
|
||||||
└─ Returns: CustomRequestDetail | DealerClaimRequestDetail component
|
|
||||||
↓
|
|
||||||
Step 4: Render Flow-Specific Screen
|
|
||||||
└─ <RequestDetailScreen {...props} />
|
|
||||||
├─ If CUSTOM → src/custom/pages/RequestDetail.tsx
|
|
||||||
└─ If DEALER_CLAIM → src/dealer-claim/pages/RequestDetail.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step-by-Step Breakdown
|
|
||||||
|
|
||||||
### Step 1: Fetch Request Data
|
|
||||||
|
|
||||||
**File**: `src/pages/RequestDetail/RequestDetail.tsx` (lines 75-79)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const {
|
|
||||||
apiRequest,
|
|
||||||
loading: requestLoading,
|
|
||||||
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
- `useRequestDetails` hook fetches the request from API
|
|
||||||
- Returns `apiRequest` object with all request data
|
|
||||||
- Includes: `workflowType`, `templateType`, `templateName`, etc.
|
|
||||||
|
|
||||||
### Step 2: Determine Flow Type
|
|
||||||
|
|
||||||
**File**: `src/pages/RequestDetail/RequestDetail.tsx` (line 94)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const flowType = getRequestFlowType(apiRequest);
|
|
||||||
```
|
|
||||||
|
|
||||||
**File**: `src/utils/requestTypeUtils.ts` (lines 70-75)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export function getRequestFlowType(request: any): RequestFlowType {
|
|
||||||
if (isDealerClaimRequest(request)) {
|
|
||||||
return 'DEALER_CLAIM';
|
|
||||||
}
|
|
||||||
return 'CUSTOM';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Detection Logic:**
|
|
||||||
- Checks `request.workflowType === 'CLAIM_MANAGEMENT'` → `DEALER_CLAIM`
|
|
||||||
- Checks `request.templateType === 'claim-management'` → `DEALER_CLAIM`
|
|
||||||
- Checks `request.templateName === 'Claim Management'` → `DEALER_CLAIM`
|
|
||||||
- Otherwise → `CUSTOM` (default)
|
|
||||||
|
|
||||||
### Step 3: Get Flow-Specific Screen Component
|
|
||||||
|
|
||||||
**File**: `src/pages/RequestDetail/RequestDetail.tsx` (line 95)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const RequestDetailScreen = getRequestDetailScreen(flowType);
|
|
||||||
```
|
|
||||||
|
|
||||||
**File**: `src/flows.ts` (lines 81-89)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.DealerClaimRequestDetail; // From src/dealer-claim/
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail; // From src/custom/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
- `getRequestDetailScreen('DEALER_CLAIM')` → Returns `DealerClaimRequestDetail` component
|
|
||||||
- `getRequestDetailScreen('CUSTOM')` → Returns `CustomRequestDetail` component
|
|
||||||
|
|
||||||
**Component Sources:**
|
|
||||||
- `DealerClaimRequestDetail` → `src/dealer-claim/pages/RequestDetail.tsx`
|
|
||||||
- `CustomRequestDetail` → `src/custom/pages/RequestDetail.tsx`
|
|
||||||
|
|
||||||
### Step 4: Render Flow-Specific Screen
|
|
||||||
|
|
||||||
**File**: `src/pages/RequestDetail/RequestDetail.tsx` (lines 99-105)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
return (
|
|
||||||
<RequestDetailScreen
|
|
||||||
requestId={propRequestId}
|
|
||||||
onBack={onBack}
|
|
||||||
dynamicRequests={dynamicRequests}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens:**
|
|
||||||
- Renders the flow-specific `RequestDetail` component
|
|
||||||
- Each flow has its own complete implementation
|
|
||||||
- All props are passed through
|
|
||||||
|
|
||||||
## Import Chain
|
|
||||||
|
|
||||||
```
|
|
||||||
src/pages/RequestDetail/RequestDetail.tsx
|
|
||||||
↓ imports
|
|
||||||
src/flows.ts
|
|
||||||
↓ imports
|
|
||||||
src/custom/index.ts → exports CustomRequestDetail
|
|
||||||
↓ imports
|
|
||||||
src/custom/pages/RequestDetail.tsx → CustomRequestDetail component
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
src/flows.ts
|
|
||||||
↓ imports
|
|
||||||
src/dealer-claim/index.ts → exports DealerClaimRequestDetail
|
|
||||||
↓ imports
|
|
||||||
src/dealer-claim/pages/RequestDetail.tsx → DealerClaimRequestDetail component
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
### 1. Router Component
|
|
||||||
- **File**: `src/pages/RequestDetail/RequestDetail.tsx`
|
|
||||||
- **Role**: Determines flow type and routes to appropriate screen
|
|
||||||
- **Key Functions**:
|
|
||||||
- Fetches request data
|
|
||||||
- Determines flow type
|
|
||||||
- Gets flow-specific screen component
|
|
||||||
- Renders the screen
|
|
||||||
|
|
||||||
### 2. Flow Registry
|
|
||||||
- **File**: `src/flows.ts`
|
|
||||||
- **Role**: Central registry for all flow types
|
|
||||||
- **Key Function**: `getRequestDetailScreen(flowType)` - Returns the appropriate screen component
|
|
||||||
|
|
||||||
### 3. Flow Type Detection
|
|
||||||
- **File**: `src/utils/requestTypeUtils.ts`
|
|
||||||
- **Role**: Detects request type from request data
|
|
||||||
- **Key Function**: `getRequestFlowType(request)` - Returns 'CUSTOM' or 'DEALER_CLAIM'
|
|
||||||
|
|
||||||
### 4. Flow-Specific Screens
|
|
||||||
- **Custom**: `src/custom/pages/RequestDetail.tsx` → `CustomRequestDetail`
|
|
||||||
- **Dealer Claim**: `src/dealer-claim/pages/RequestDetail.tsx` → `DealerClaimRequestDetail`
|
|
||||||
|
|
||||||
## Example Flow
|
|
||||||
|
|
||||||
### Example 1: Custom Request
|
|
||||||
|
|
||||||
```
|
|
||||||
1. User clicks Custom Request card
|
|
||||||
2. Navigate to: /request/REQ-2024-001
|
|
||||||
3. RequestDetail.tsx loads
|
|
||||||
4. useRequestDetails fetches: { workflowType: 'CUSTOM', ... }
|
|
||||||
5. getRequestFlowType() → 'CUSTOM'
|
|
||||||
6. getRequestDetailScreen('CUSTOM') → CustomRequestDetail component
|
|
||||||
7. Renders: src/custom/pages/RequestDetail.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Dealer Claim Request
|
|
||||||
|
|
||||||
```
|
|
||||||
1. User clicks Dealer Claim card
|
|
||||||
2. Navigate to: /request/REQ-2024-002
|
|
||||||
3. RequestDetail.tsx loads
|
|
||||||
4. useRequestDetails fetches: { workflowType: 'CLAIM_MANAGEMENT', ... }
|
|
||||||
5. getRequestFlowType() → 'DEALER_CLAIM'
|
|
||||||
6. getRequestDetailScreen('DEALER_CLAIM') → DealerClaimRequestDetail component
|
|
||||||
7. Renders: src/dealer-claim/pages/RequestDetail.tsx
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits of This Architecture
|
|
||||||
|
|
||||||
1. **Single Entry Point**: All requests go through `/request/{id}` route
|
|
||||||
2. **Dynamic Routing**: Flow type determined at runtime from request data
|
|
||||||
3. **Modular**: Each flow is completely self-contained
|
|
||||||
4. **Easy to Extend**: Add new flow type by:
|
|
||||||
- Create `src/new-flow/` folder
|
|
||||||
- Add to `src/flows.ts` registry
|
|
||||||
- Done!
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
The routing works in **4 simple steps**:
|
|
||||||
|
|
||||||
1. **Fetch** → Get request data from API
|
|
||||||
2. **Detect** → Determine flow type from request properties
|
|
||||||
3. **Resolve** → Get flow-specific screen component from registry
|
|
||||||
4. **Render** → Display the appropriate screen
|
|
||||||
|
|
||||||
All of this happens automatically based on the request data - no manual routing needed!
|
|
||||||
|
|
||||||
@ -1,222 +0,0 @@
|
|||||||
# Source-Level Flow Structure - Complete Implementation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Flow folders are now at the **`src/` level** for maximum visibility and easy removal. Each flow folder is completely self-contained - deleting a folder removes ALL related code.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── custom/ # Custom Request Flow (COMPLETE)
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/
|
|
||||||
│ │ │ ├── OverviewTab.tsx # Custom overview
|
|
||||||
│ │ │ └── WorkflowTab.tsx # Custom workflow
|
|
||||||
│ │ └── request-creation/
|
|
||||||
│ │ └── CreateRequest.tsx # Custom creation
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # COMPLETE Custom RequestDetail screen
|
|
||||||
│ ├── hooks/ # Custom-specific hooks (future)
|
|
||||||
│ ├── services/ # Custom-specific services (future)
|
|
||||||
│ ├── utils/ # Custom-specific utilities (future)
|
|
||||||
│ ├── types/ # Custom-specific types (future)
|
|
||||||
│ └── index.ts # Exports all Custom components
|
|
||||||
│
|
|
||||||
├── dealer-claim/ # Dealer Claim Flow (COMPLETE)
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── request-detail/
|
|
||||||
│ │ │ ├── OverviewTab.tsx # Dealer claim overview
|
|
||||||
│ │ │ ├── WorkflowTab.tsx # Dealer claim workflow
|
|
||||||
│ │ │ ├── IOTab.tsx # IO management
|
|
||||||
│ │ │ ├── claim-cards/ # All dealer claim cards
|
|
||||||
│ │ │ │ ├── ActivityInformationCard.tsx
|
|
||||||
│ │ │ │ ├── DealerInformationCard.tsx
|
|
||||||
│ │ │ │ ├── ProcessDetailsCard.tsx
|
|
||||||
│ │ │ │ ├── ProposalDetailsCard.tsx
|
|
||||||
│ │ │ │ └── RequestInitiatorCard.tsx
|
|
||||||
│ │ │ └── modals/ # All dealer claim modals
|
|
||||||
│ │ │ ├── CreditNoteSAPModal.tsx
|
|
||||||
│ │ │ ├── DealerCompletionDocumentsModal.tsx
|
|
||||||
│ │ │ ├── DealerProposalSubmissionModal.tsx
|
|
||||||
│ │ │ ├── DeptLeadIOApprovalModal.tsx
|
|
||||||
│ │ │ ├── EditClaimAmountModal.tsx
|
|
||||||
│ │ │ ├── EmailNotificationTemplateModal.tsx
|
|
||||||
│ │ │ └── InitiatorProposalApprovalModal.tsx
|
|
||||||
│ │ └── request-creation/
|
|
||||||
│ │ └── ClaimManagementWizard.tsx
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ └── RequestDetail.tsx # COMPLETE Dealer Claim RequestDetail screen
|
|
||||||
│ ├── hooks/ # Dealer claim hooks (future)
|
|
||||||
│ ├── services/ # Dealer claim services (future)
|
|
||||||
│ ├── utils/ # Dealer claim utilities (future)
|
|
||||||
│ ├── types/ # Dealer claim types (future)
|
|
||||||
│ └── index.ts # Exports all Dealer Claim components
|
|
||||||
│
|
|
||||||
├── shared/ # Shared Components (Flow-Agnostic)
|
|
||||||
│ └── components/
|
|
||||||
│ └── request-detail/
|
|
||||||
│ ├── DocumentsTab.tsx # Used by all flows
|
|
||||||
│ ├── ActivityTab.tsx # Used by all flows
|
|
||||||
│ ├── WorkNotesTab.tsx # Used by all flows
|
|
||||||
│ ├── SummaryTab.tsx # Used by all flows
|
|
||||||
│ ├── RequestDetailHeader.tsx
|
|
||||||
│ ├── QuickActionsSidebar.tsx
|
|
||||||
│ └── RequestDetailModals.tsx
|
|
||||||
│
|
|
||||||
└── flows.ts # Flow registry and routing utilities
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### ✅ Source-Level Visibility
|
|
||||||
- Flow folders are directly under `src/`
|
|
||||||
- Easy to see and navigate
|
|
||||||
- Clear separation from other code
|
|
||||||
|
|
||||||
### ✅ Complete Self-Containment
|
|
||||||
- Each flow folder contains ALL its code
|
|
||||||
- RequestDetail screens, components, modals, cards
|
|
||||||
- Future: hooks, services, utils, types
|
|
||||||
|
|
||||||
### ✅ Easy Removal
|
|
||||||
- Delete `src/custom/` → All custom code removed
|
|
||||||
- Delete `src/dealer-claim/` → All dealer claim code removed
|
|
||||||
- Update `src/flows.ts` → Done!
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### Main RequestDetail Router
|
|
||||||
|
|
||||||
`src/pages/RequestDetail/RequestDetail.tsx` is a simple router:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. Fetches request to determine flow type
|
|
||||||
const flowType = getRequestFlowType(apiRequest);
|
|
||||||
|
|
||||||
// 2. Gets the appropriate RequestDetail screen from registry
|
|
||||||
const RequestDetailScreen = getRequestDetailScreen(flowType);
|
|
||||||
|
|
||||||
// 3. Renders the flow-specific screen
|
|
||||||
return <RequestDetailScreen {...props} />;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flow Registry
|
|
||||||
|
|
||||||
`src/flows.ts` contains the registry:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import * as CustomFlow from './custom';
|
|
||||||
import * as DealerClaimFlow from './dealer-claim';
|
|
||||||
import * as SharedComponents from './shared/components';
|
|
||||||
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.DealerClaimRequestDetail;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deleting a Flow
|
|
||||||
|
|
||||||
### Example: Remove Dealer Claim
|
|
||||||
|
|
||||||
**Step 1:** Delete folder
|
|
||||||
```bash
|
|
||||||
rm -rf src/dealer-claim/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2:** Update `src/flows.ts`
|
|
||||||
```typescript
|
|
||||||
// Remove import
|
|
||||||
// import * as DealerClaimFlow from './dealer-claim';
|
|
||||||
|
|
||||||
// Update FlowRegistry
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
// DEALER_CLAIM: DealerClaimFlow, // REMOVED
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Update getRequestDetailScreen()
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
// case 'DEALER_CLAIM': // REMOVED
|
|
||||||
// return DealerClaimFlow.DealerClaimRequestDetail;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Done!** All dealer claim code is removed.
|
|
||||||
|
|
||||||
## Import Paths
|
|
||||||
|
|
||||||
### Flow-Specific Imports
|
|
||||||
```typescript
|
|
||||||
// Custom flow
|
|
||||||
import { CustomRequestDetail } from '@/custom';
|
|
||||||
import { CustomOverviewTab } from '@/custom';
|
|
||||||
|
|
||||||
// Dealer claim flow
|
|
||||||
import { DealerClaimRequestDetail } from '@/dealer-claim';
|
|
||||||
import { DealerClaimOverviewTab } from '@/dealer-claim';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Shared Components
|
|
||||||
```typescript
|
|
||||||
import { SharedComponents } from '@/shared/components';
|
|
||||||
const { DocumentsTab, ActivityTab } = SharedComponents;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Flow Registry
|
|
||||||
```typescript
|
|
||||||
import { getRequestDetailScreen, CustomFlow, DealerClaimFlow } from '@/flows';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
1. **Maximum Visibility**: Flow folders at `src/` level are immediately visible
|
|
||||||
2. **Easy Navigation**: No nested paths - just `src/custom/` or `src/dealer-claim/`
|
|
||||||
3. **Simple Removal**: Delete folder + update `flows.ts` = Done
|
|
||||||
4. **Clear Ownership**: Know exactly what belongs to which flow
|
|
||||||
5. **Complete Isolation**: Each flow is independent
|
|
||||||
6. **Future-Proof**: Easy to add hooks, services, utils, types to each flow
|
|
||||||
|
|
||||||
## Current Structure
|
|
||||||
|
|
||||||
### `src/custom/`
|
|
||||||
- ✅ Complete RequestDetail screen
|
|
||||||
- ✅ Request detail components
|
|
||||||
- ✅ Request creation component
|
|
||||||
- 🔜 Hooks, services, utils, types
|
|
||||||
|
|
||||||
### `src/dealer-claim/`
|
|
||||||
- ✅ Complete RequestDetail screen
|
|
||||||
- ✅ Request detail components
|
|
||||||
- ✅ Request detail cards (5 cards)
|
|
||||||
- ✅ Request detail modals (7 modals)
|
|
||||||
- ✅ Request creation wizard
|
|
||||||
- 🔜 Hooks, services, utils, types
|
|
||||||
|
|
||||||
### `src/shared/`
|
|
||||||
- ✅ Shared components used by all flows
|
|
||||||
|
|
||||||
### `src/flows.ts`
|
|
||||||
- ✅ Flow registry
|
|
||||||
- ✅ Routing utilities
|
|
||||||
- ✅ Component getters
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The architecture is now **completely modular at the source level**. Flow folders are directly under `src/` for maximum visibility and easy removal. Deleting a folder removes all related code with zero dependencies remaining.
|
|
||||||
25
src/App.tsx
25
src/App.tsx
@ -109,19 +109,17 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewRequest = async (requestId: string, requestTitle?: string, status?: string, request?: any) => {
|
const handleViewRequest = async (requestId: string, requestTitle?: string, status?: string) => {
|
||||||
setSelectedRequestId(requestId);
|
setSelectedRequestId(requestId);
|
||||||
setSelectedRequestTitle(requestTitle || 'Unknown Request');
|
setSelectedRequestTitle(requestTitle || 'Unknown Request');
|
||||||
|
|
||||||
// Use global navigation utility for consistent routing
|
// Check if request is a draft - if so, route to edit form instead of detail view
|
||||||
const { navigateToRequest } = await import('@/utils/requestNavigation');
|
const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';
|
||||||
navigateToRequest({
|
if (isDraft) {
|
||||||
requestId,
|
navigate(`/edit-request/${requestId}`);
|
||||||
requestTitle,
|
} else {
|
||||||
status,
|
navigate(`/request/${requestId}`);
|
||||||
request,
|
}
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@ -309,12 +307,7 @@ function AppRoutes({ onLogout }: AppProps) {
|
|||||||
|
|
||||||
// Navigate to the created request detail page
|
// Navigate to the created request detail page
|
||||||
if (createdRequest?.requestId) {
|
if (createdRequest?.requestId) {
|
||||||
const { navigateToRequest } = await import('@/utils/requestNavigation');
|
navigate(`/request/${createdRequest.requestId}`);
|
||||||
navigateToRequest({
|
|
||||||
requestId: createdRequest.requestId,
|
|
||||||
request: createdRequest,
|
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
navigate('/my-requests');
|
navigate('/my-requests');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,10 @@ import { toast } from 'sonner';
|
|||||||
|
|
||||||
interface AIConfigData {
|
interface AIConfigData {
|
||||||
aiEnabled: boolean;
|
aiEnabled: boolean;
|
||||||
|
aiProvider: 'claude' | 'openai' | 'gemini';
|
||||||
|
claudeApiKey: string;
|
||||||
|
openaiApiKey: string;
|
||||||
|
geminiApiKey: string;
|
||||||
aiRemarkGeneration: boolean;
|
aiRemarkGeneration: boolean;
|
||||||
maxRemarkChars: number;
|
maxRemarkChars: number;
|
||||||
}
|
}
|
||||||
@ -18,10 +22,19 @@ interface AIConfigData {
|
|||||||
export function AIConfig() {
|
export function AIConfig() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [showApiKeys, setShowApiKeys] = useState<Record<string, boolean>>({
|
||||||
|
claude: false,
|
||||||
|
openai: false,
|
||||||
|
gemini: false
|
||||||
|
});
|
||||||
const [config, setConfig] = useState<AIConfigData>({
|
const [config, setConfig] = useState<AIConfigData>({
|
||||||
aiEnabled: true,
|
aiEnabled: true,
|
||||||
|
aiProvider: 'claude',
|
||||||
|
claudeApiKey: '',
|
||||||
|
openaiApiKey: '',
|
||||||
|
geminiApiKey: '',
|
||||||
aiRemarkGeneration: true,
|
aiRemarkGeneration: true,
|
||||||
maxRemarkChars: 2000
|
maxRemarkChars: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -41,6 +54,10 @@ export function AIConfig() {
|
|||||||
|
|
||||||
setConfig({
|
setConfig({
|
||||||
aiEnabled: configMap['AI_ENABLED'] === 'true',
|
aiEnabled: configMap['AI_ENABLED'] === 'true',
|
||||||
|
aiProvider: (configMap['AI_PROVIDER'] || 'claude') as 'claude' | 'openai' | 'gemini',
|
||||||
|
claudeApiKey: configMap['CLAUDE_API_KEY'] || '',
|
||||||
|
openaiApiKey: configMap['OPENAI_API_KEY'] || '',
|
||||||
|
geminiApiKey: configMap['GEMINI_API_KEY'] || '',
|
||||||
aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true',
|
aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true',
|
||||||
maxRemarkChars: parseInt(configMap['AI_MAX_REMARK_LENGTH'] || '2000')
|
maxRemarkChars: parseInt(configMap['AI_MAX_REMARK_LENGTH'] || '2000')
|
||||||
});
|
});
|
||||||
@ -59,6 +76,10 @@ export function AIConfig() {
|
|||||||
// Save all configurations
|
// Save all configurations
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updateConfiguration('AI_ENABLED', config.aiEnabled.toString()),
|
updateConfiguration('AI_ENABLED', config.aiEnabled.toString()),
|
||||||
|
updateConfiguration('AI_PROVIDER', config.aiProvider),
|
||||||
|
updateConfiguration('CLAUDE_API_KEY', config.claudeApiKey),
|
||||||
|
updateConfiguration('OPENAI_API_KEY', config.openaiApiKey),
|
||||||
|
updateConfiguration('GEMINI_API_KEY', config.geminiApiKey),
|
||||||
updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()),
|
updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()),
|
||||||
updateConfiguration('AI_MAX_REMARK_LENGTH', config.maxRemarkChars.toString())
|
updateConfiguration('AI_MAX_REMARK_LENGTH', config.maxRemarkChars.toString())
|
||||||
]);
|
]);
|
||||||
@ -79,6 +100,19 @@ export function AIConfig() {
|
|||||||
setConfig(prev => ({ ...prev, ...updates }));
|
setConfig(prev => ({ ...prev, ...updates }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleApiKeyVisibility = (provider: 'claude' | 'openai' | 'gemini') => {
|
||||||
|
setShowApiKeys(prev => ({
|
||||||
|
...prev,
|
||||||
|
[provider]: !prev[provider]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const maskApiKey = (key: string): string => {
|
||||||
|
if (!key || key.length === 0) return '';
|
||||||
|
if (key.length <= 8) return '••••••••';
|
||||||
|
return key.substring(0, 4) + '••••••••' + key.substring(key.length - 4);
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Card className="shadow-lg border-0 rounded-md">
|
<Card className="shadow-lg border-0 rounded-md">
|
||||||
@ -100,7 +134,7 @@ export function AIConfig() {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-lg font-semibold text-gray-900">AI Features Configuration</CardTitle>
|
<CardTitle className="text-lg font-semibold text-gray-900">AI Features Configuration</CardTitle>
|
||||||
<CardDescription className="text-sm text-gray-600">
|
<CardDescription className="text-sm text-gray-600">
|
||||||
Configure Vertex AI Gemini settings and enable/disable AI-powered features
|
Configure AI provider, API keys, and enable/disable AI-powered features
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -108,7 +142,18 @@ export function AIConfig() {
|
|||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<AIProviderSettings
|
<AIProviderSettings
|
||||||
aiEnabled={config.aiEnabled}
|
aiEnabled={config.aiEnabled}
|
||||||
|
aiProvider={config.aiProvider}
|
||||||
|
claudeApiKey={config.claudeApiKey}
|
||||||
|
openaiApiKey={config.openaiApiKey}
|
||||||
|
geminiApiKey={config.geminiApiKey}
|
||||||
|
showApiKeys={showApiKeys}
|
||||||
onAiEnabledChange={(enabled) => updateConfig({ aiEnabled: enabled })}
|
onAiEnabledChange={(enabled) => updateConfig({ aiEnabled: enabled })}
|
||||||
|
onProviderChange={(provider) => updateConfig({ aiProvider: provider })}
|
||||||
|
onClaudeApiKeyChange={(key) => updateConfig({ claudeApiKey: key })}
|
||||||
|
onOpenaiApiKeyChange={(key) => updateConfig({ openaiApiKey: key })}
|
||||||
|
onGeminiApiKeyChange={(key) => updateConfig({ geminiApiKey: key })}
|
||||||
|
onToggleApiKeyVisibility={toggleApiKeyVisibility}
|
||||||
|
maskApiKey={maskApiKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@ -1,25 +1,63 @@
|
|||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Brain } from 'lucide-react';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Brain, Eye, EyeOff, Key } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
interface AIProviderSettingsProps {
|
interface AIProviderSettingsProps {
|
||||||
aiEnabled: boolean;
|
aiEnabled: boolean;
|
||||||
|
aiProvider: 'claude' | 'openai' | 'gemini';
|
||||||
|
claudeApiKey: string;
|
||||||
|
openaiApiKey: string;
|
||||||
|
geminiApiKey: string;
|
||||||
|
showApiKeys: Record<string, boolean>;
|
||||||
onAiEnabledChange: (enabled: boolean) => void;
|
onAiEnabledChange: (enabled: boolean) => void;
|
||||||
|
onProviderChange: (provider: 'claude' | 'openai' | 'gemini') => void;
|
||||||
|
onClaudeApiKeyChange: (key: string) => void;
|
||||||
|
onOpenaiApiKeyChange: (key: string) => void;
|
||||||
|
onGeminiApiKeyChange: (key: string) => void;
|
||||||
|
onToggleApiKeyVisibility: (provider: 'claude' | 'openai' | 'gemini') => void;
|
||||||
|
maskApiKey: (key: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROVIDERS = [
|
||||||
|
{ value: 'claude', label: 'Claude (Anthropic)', description: 'Advanced AI by Anthropic' },
|
||||||
|
{ value: 'openai', label: 'OpenAI (GPT-4)', description: 'GPT-4 by OpenAI' },
|
||||||
|
{ value: 'gemini', label: 'Gemini (Google)', description: 'Gemini by Google' }
|
||||||
|
];
|
||||||
|
|
||||||
export function AIProviderSettings({
|
export function AIProviderSettings({
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
onAiEnabledChange
|
aiProvider,
|
||||||
|
claudeApiKey,
|
||||||
|
openaiApiKey,
|
||||||
|
geminiApiKey,
|
||||||
|
showApiKeys,
|
||||||
|
onAiEnabledChange,
|
||||||
|
onProviderChange,
|
||||||
|
onClaudeApiKeyChange,
|
||||||
|
onOpenaiApiKeyChange,
|
||||||
|
onGeminiApiKeyChange,
|
||||||
|
onToggleApiKeyVisibility,
|
||||||
|
maskApiKey
|
||||||
}: AIProviderSettingsProps) {
|
}: AIProviderSettingsProps) {
|
||||||
return (
|
return (
|
||||||
<Card className="border-0 shadow-sm">
|
<Card className="border-0 shadow-sm">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Brain className="w-5 h-5 text-re-green" />
|
<Brain className="w-5 h-5 text-re-green" />
|
||||||
<CardTitle className="text-base font-semibold">Vertex AI Gemini Configuration</CardTitle>
|
<CardTitle className="text-base font-semibold">AI Provider & API Keys</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="text-sm">
|
<CardDescription className="text-sm">
|
||||||
Configure AI features. Model and region are configured via environment variables.
|
Select your AI provider and configure API keys
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@ -36,7 +74,145 @@ export function AIProviderSettings({
|
|||||||
onCheckedChange={onAiEnabledChange}
|
onCheckedChange={onAiEnabledChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{aiEnabled && (
|
||||||
|
<>
|
||||||
|
{/* Provider Selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ai-provider" className="text-sm font-medium">
|
||||||
|
AI Provider
|
||||||
|
</Label>
|
||||||
|
<Select value={aiProvider} onValueChange={(value: any) => onProviderChange(value)}>
|
||||||
|
<SelectTrigger
|
||||||
|
id="ai-provider"
|
||||||
|
className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select AI provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PROVIDERS.map((provider) => (
|
||||||
|
<SelectItem key={provider.value} value={provider.value}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{provider.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">-</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{provider.description}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Keys for each provider */}
|
||||||
|
<div className="space-y-4 pt-2">
|
||||||
|
<Label className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Key className="w-4 h-4" />
|
||||||
|
API Keys
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{/* Claude API Key */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="claude-key" className="text-xs text-muted-foreground">
|
||||||
|
Claude API Key
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="claude-key"
|
||||||
|
type={showApiKeys.claude ? 'text' : 'password'}
|
||||||
|
value={claudeApiKey}
|
||||||
|
onChange={(e) => onClaudeApiKeyChange(e.target.value)}
|
||||||
|
placeholder={showApiKeys.claude ? "sk-ant-..." : maskApiKey(claudeApiKey) || "sk-ant-..."}
|
||||||
|
className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onToggleApiKeyVisibility('claude')}
|
||||||
|
className="border-gray-200"
|
||||||
|
>
|
||||||
|
{showApiKeys.claude ? (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get your API key from console.anthropic.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OpenAI API Key */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="openai-key" className="text-xs text-muted-foreground">
|
||||||
|
OpenAI API Key
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="openai-key"
|
||||||
|
type={showApiKeys.openai ? 'text' : 'password'}
|
||||||
|
value={openaiApiKey}
|
||||||
|
onChange={(e) => onOpenaiApiKeyChange(e.target.value)}
|
||||||
|
placeholder={showApiKeys.openai ? "sk-..." : maskApiKey(openaiApiKey) || "sk-..."}
|
||||||
|
className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onToggleApiKeyVisibility('openai')}
|
||||||
|
className="border-gray-200"
|
||||||
|
>
|
||||||
|
{showApiKeys.openai ? (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get your API key from platform.openai.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gemini API Key */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="gemini-key" className="text-xs text-muted-foreground">
|
||||||
|
Gemini API Key
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="gemini-key"
|
||||||
|
type={showApiKeys.gemini ? 'text' : 'password'}
|
||||||
|
value={geminiApiKey}
|
||||||
|
onChange={(e) => onGeminiApiKeyChange(e.target.value)}
|
||||||
|
placeholder={showApiKeys.gemini ? "AIza..." : maskApiKey(geminiApiKey) || "AIza..."}
|
||||||
|
className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onToggleApiKeyVisibility('gemini')}
|
||||||
|
className="border-gray-200"
|
||||||
|
>
|
||||||
|
{showApiKeys.gemini ? (
|
||||||
|
<EyeOff className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Get your API key from ai.google.dev
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { cn } from "./utils";
|
import { cn } from "./utils";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
import { Bold, Italic, Underline, List, ListOrdered, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight, Highlighter, X, Type } from "lucide-react";
|
import { Bold, Italic, Underline, List, ListOrdered, Heading1, Heading2, Heading3, AlignLeft, AlignCenter, AlignRight } from "lucide-react";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
||||||
|
|
||||||
interface RichTextEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
interface RichTextEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||||
value: string;
|
value: string;
|
||||||
@ -12,23 +11,6 @@ interface RichTextEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
|||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Predefined highlight colors - 12 major colors in 2 rows (6 columns x 2 rows)
|
|
||||||
// "No Color" is first for easy access (standard pattern)
|
|
||||||
const HIGHLIGHT_COLORS = [
|
|
||||||
{ name: 'No Color', value: 'transparent', class: 'bg-gray-200 border-2 border-gray-400', icon: '×' },
|
|
||||||
{ name: 'Yellow', value: '#FFEB3B', class: 'bg-yellow-400' },
|
|
||||||
{ name: 'Green', value: '#4CAF50', class: 'bg-green-400' },
|
|
||||||
{ name: 'Blue', value: '#2196F3', class: 'bg-blue-400' },
|
|
||||||
{ name: 'Red', value: '#F44336', class: 'bg-red-400' },
|
|
||||||
{ name: 'Orange', value: '#FF9800', class: 'bg-orange-400' },
|
|
||||||
{ name: 'Purple', value: '#9C27B0', class: 'bg-purple-400' },
|
|
||||||
{ name: 'Pink', value: '#E91E63', class: 'bg-pink-400' },
|
|
||||||
{ name: 'Cyan', value: '#00BCD4', class: 'bg-cyan-400' },
|
|
||||||
{ name: 'Teal', value: '#009688', class: 'bg-teal-400' },
|
|
||||||
{ name: 'Amber', value: '#FFC107', class: 'bg-amber-400' },
|
|
||||||
{ name: 'Indigo', value: '#3F51B5', class: 'bg-indigo-400' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RichTextEditor Component
|
* RichTextEditor Component
|
||||||
*
|
*
|
||||||
@ -46,12 +28,6 @@ export function RichTextEditor({
|
|||||||
const editorRef = React.useRef<HTMLDivElement>(null);
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isFocused, setIsFocused] = React.useState(false);
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
const [activeFormats, setActiveFormats] = React.useState<Set<string>>(new Set());
|
const [activeFormats, setActiveFormats] = React.useState<Set<string>>(new Set());
|
||||||
const [highlightColorOpen, setHighlightColorOpen] = React.useState(false);
|
|
||||||
const [currentHighlightColor, setCurrentHighlightColor] = React.useState<string | null>(null);
|
|
||||||
const [customColor, setCustomColor] = React.useState<string>('#FFEB3B');
|
|
||||||
const [textColorOpen, setTextColorOpen] = React.useState(false);
|
|
||||||
const [currentTextColor, setCurrentTextColor] = React.useState<string | null>(null);
|
|
||||||
const [customTextColor, setCustomTextColor] = React.useState<string>('#000000');
|
|
||||||
|
|
||||||
// Sync external value to editor
|
// Sync external value to editor
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -273,84 +249,6 @@ export function RichTextEditor({
|
|||||||
if (style.textAlign === 'right') formats.add('right');
|
if (style.textAlign === 'right') formats.add('right');
|
||||||
if (style.textAlign === 'left') formats.add('left');
|
if (style.textAlign === 'left') formats.add('left');
|
||||||
|
|
||||||
// Convert RGB/RGBA to hex for comparison
|
|
||||||
const colorToHex = (color: string): string | null => {
|
|
||||||
// If already hex format
|
|
||||||
if (color.startsWith('#')) {
|
|
||||||
return color.toUpperCase();
|
|
||||||
}
|
|
||||||
// If RGB/RGBA format
|
|
||||||
const result = color.match(/\d+/g);
|
|
||||||
if (!result || result.length < 3) return null;
|
|
||||||
const r = result[0];
|
|
||||||
const g = result[1];
|
|
||||||
const b = result[2];
|
|
||||||
if (!r || !g || !b) return null;
|
|
||||||
const rHex = parseInt(r).toString(16).padStart(2, '0');
|
|
||||||
const gHex = parseInt(g).toString(16).padStart(2, '0');
|
|
||||||
const bHex = parseInt(b).toString(16).padStart(2, '0');
|
|
||||||
return `#${rHex}${gHex}${bHex}`.toUpperCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for background color (highlight)
|
|
||||||
const bgColor = style.backgroundColor;
|
|
||||||
// Check if background color is set and not transparent/default
|
|
||||||
if (bgColor &&
|
|
||||||
bgColor !== 'rgba(0, 0, 0, 0)' &&
|
|
||||||
bgColor !== 'transparent' &&
|
|
||||||
bgColor !== 'rgb(255, 255, 255)' &&
|
|
||||||
bgColor !== '#ffffff' &&
|
|
||||||
bgColor !== '#FFFFFF') {
|
|
||||||
formats.add('highlight');
|
|
||||||
const hexColor = colorToHex(bgColor);
|
|
||||||
if (hexColor) {
|
|
||||||
// Find matching color from our palette (allowing for slight variations)
|
|
||||||
const matchedColor = HIGHLIGHT_COLORS.find(c => {
|
|
||||||
if (c.value === 'transparent') return false;
|
|
||||||
// Compare hex values (case-insensitive)
|
|
||||||
return c.value.toUpperCase() === hexColor;
|
|
||||||
});
|
|
||||||
if (matchedColor) {
|
|
||||||
setCurrentHighlightColor(matchedColor.value);
|
|
||||||
} else {
|
|
||||||
// Store the actual color even if not in our palette
|
|
||||||
setCurrentHighlightColor(hexColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!formats.has('highlight')) {
|
|
||||||
// Only reset if we haven't found a highlight yet
|
|
||||||
setCurrentHighlightColor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for text color
|
|
||||||
const textColor = style.color;
|
|
||||||
// Convert to hex for comparison
|
|
||||||
const hexTextColor = colorToHex(textColor);
|
|
||||||
// Check if text color is set and not default black
|
|
||||||
if (textColor && hexTextColor &&
|
|
||||||
textColor !== 'rgba(0, 0, 0, 0)' &&
|
|
||||||
hexTextColor !== '#000000') {
|
|
||||||
formats.add('textColor');
|
|
||||||
// Find matching color from our palette
|
|
||||||
const matchedColor = HIGHLIGHT_COLORS.find(c => {
|
|
||||||
if (c.value === 'transparent') return false;
|
|
||||||
return c.value.toUpperCase() === hexTextColor;
|
|
||||||
});
|
|
||||||
if (matchedColor) {
|
|
||||||
setCurrentTextColor(matchedColor.value);
|
|
||||||
} else {
|
|
||||||
// Store the actual color even if not in our palette
|
|
||||||
setCurrentTextColor(hexTextColor);
|
|
||||||
}
|
|
||||||
} else if (!formats.has('textColor')) {
|
|
||||||
// Only reset if we haven't found a text color yet (default to black)
|
|
||||||
if (hexTextColor === '#000000' || !hexTextColor) {
|
|
||||||
setCurrentTextColor('#000000');
|
|
||||||
} else {
|
|
||||||
setCurrentTextColor(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
element = element.parentElement;
|
element = element.parentElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -387,265 +285,6 @@ export function RichTextEditor({
|
|||||||
setTimeout(checkActiveFormats, 10);
|
setTimeout(checkActiveFormats, 10);
|
||||||
}, [isFocused, onChange, checkActiveFormats]);
|
}, [isFocused, onChange, checkActiveFormats]);
|
||||||
|
|
||||||
// Apply highlight color
|
|
||||||
const applyHighlight = React.useCallback((color: string) => {
|
|
||||||
if (!editorRef.current) return;
|
|
||||||
|
|
||||||
// Restore focus if needed
|
|
||||||
if (!isFocused) {
|
|
||||||
editorRef.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current selection
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || selection.rangeCount === 0) {
|
|
||||||
// If no selection, focus the editor
|
|
||||||
editorRef.current.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this color is already applied by checking the selection's style
|
|
||||||
let isAlreadyApplied = false;
|
|
||||||
if (selection.rangeCount > 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const commonAncestor = range.commonAncestorContainer;
|
|
||||||
let element: HTMLElement | null = null;
|
|
||||||
|
|
||||||
if (commonAncestor.nodeType === Node.TEXT_NODE) {
|
|
||||||
element = commonAncestor.parentElement;
|
|
||||||
} else {
|
|
||||||
element = commonAncestor as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the selected element has the same background color
|
|
||||||
while (element && element !== editorRef.current) {
|
|
||||||
const style = window.getComputedStyle(element);
|
|
||||||
const bgColor = style.backgroundColor;
|
|
||||||
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' &&
|
|
||||||
bgColor !== 'rgb(255, 255, 255)' && bgColor !== '#ffffff' && bgColor !== '#FFFFFF') {
|
|
||||||
// Convert to hex and compare
|
|
||||||
const colorToHex = (c: string): string | null => {
|
|
||||||
if (c.startsWith('#')) return c.toUpperCase();
|
|
||||||
const result = c.match(/\d+/g);
|
|
||||||
if (!result || result.length < 3) return null;
|
|
||||||
const r = result[0];
|
|
||||||
const g = result[1];
|
|
||||||
const b = result[2];
|
|
||||||
if (!r || !g || !b) return null;
|
|
||||||
const rHex = parseInt(r).toString(16).padStart(2, '0');
|
|
||||||
const gHex = parseInt(g).toString(16).padStart(2, '0');
|
|
||||||
const bHex = parseInt(b).toString(16).padStart(2, '0');
|
|
||||||
return `#${rHex}${gHex}${bHex}`.toUpperCase();
|
|
||||||
};
|
|
||||||
const hexBgColor = colorToHex(bgColor);
|
|
||||||
if (hexBgColor && hexBgColor === color.toUpperCase()) {
|
|
||||||
isAlreadyApplied = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use backColor command for highlight (background color)
|
|
||||||
if (color === 'transparent' || isAlreadyApplied) {
|
|
||||||
// Remove highlight - use a more aggressive approach to fully remove
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
if (!range.collapsed) {
|
|
||||||
// Store the range before manipulation
|
|
||||||
const contents = range.extractContents();
|
|
||||||
|
|
||||||
// Create a new text node or span without background color
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
|
|
||||||
// Process extracted contents to remove background colors
|
|
||||||
const processNode = (node: Node) => {
|
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
return node.cloneNode(true);
|
|
||||||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const el = node as HTMLElement;
|
|
||||||
const newEl = document.createElement(el.tagName.toLowerCase());
|
|
||||||
|
|
||||||
// Copy all attributes except style-related ones
|
|
||||||
Array.from(el.attributes).forEach(attr => {
|
|
||||||
if (attr.name !== 'style' && attr.name !== 'class') {
|
|
||||||
newEl.setAttribute(attr.name, attr.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process children and copy without background color
|
|
||||||
Array.from(el.childNodes).forEach(child => {
|
|
||||||
const processed = processNode(child);
|
|
||||||
if (processed) {
|
|
||||||
newEl.appendChild(processed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove background color if present
|
|
||||||
if (el.style.backgroundColor) {
|
|
||||||
newEl.style.backgroundColor = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return newEl;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
Array.from(contents.childNodes).forEach(child => {
|
|
||||||
const processed = processNode(child);
|
|
||||||
if (processed) {
|
|
||||||
fragment.appendChild(processed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert the cleaned fragment
|
|
||||||
range.insertNode(fragment);
|
|
||||||
|
|
||||||
// Also use execCommand to ensure removal
|
|
||||||
document.execCommand('removeFormat', false);
|
|
||||||
} else {
|
|
||||||
// No selection - remove format from current position
|
|
||||||
document.execCommand('removeFormat', false);
|
|
||||||
}
|
|
||||||
setCurrentHighlightColor(null);
|
|
||||||
setCustomColor('#FFEB3B'); // Reset to default
|
|
||||||
} else {
|
|
||||||
// Apply new highlight color - only if there's a selection
|
|
||||||
if (selection.rangeCount > 0 && !selection.getRangeAt(0).collapsed) {
|
|
||||||
document.execCommand('backColor', false, color);
|
|
||||||
setCurrentHighlightColor(color);
|
|
||||||
// Update custom color input if it's a valid hex color (not transparent)
|
|
||||||
if (color !== 'transparent' && /^#[0-9A-Fa-f]{6}$/i.test(color)) {
|
|
||||||
setCustomColor(color);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No selection - don't apply (prevents sticky mode where it applies to next typed text)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear selection immediately after applying to prevent "sticky" highlight mode
|
|
||||||
const sel = window.getSelection();
|
|
||||||
if (sel) {
|
|
||||||
sel.removeAllRanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update content
|
|
||||||
if (editorRef.current) {
|
|
||||||
onChange(editorRef.current.innerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close popover
|
|
||||||
setHighlightColorOpen(false);
|
|
||||||
|
|
||||||
// Refocus editor after a short delay and check formats
|
|
||||||
setTimeout(() => {
|
|
||||||
if (editorRef.current) {
|
|
||||||
editorRef.current.focus();
|
|
||||||
}
|
|
||||||
checkActiveFormats();
|
|
||||||
}, 50);
|
|
||||||
}, [isFocused, onChange, checkActiveFormats]);
|
|
||||||
|
|
||||||
// Apply text color
|
|
||||||
const applyTextColor = React.useCallback((color: string) => {
|
|
||||||
if (!editorRef.current) return;
|
|
||||||
|
|
||||||
// Restore focus if needed
|
|
||||||
if (!isFocused) {
|
|
||||||
editorRef.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current selection
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || selection.rangeCount === 0) {
|
|
||||||
// If no selection, focus the editor
|
|
||||||
editorRef.current.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this color is already applied by checking the selection's style
|
|
||||||
let isAlreadyApplied = false;
|
|
||||||
if (selection.rangeCount > 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
const commonAncestor = range.commonAncestorContainer;
|
|
||||||
let element: HTMLElement | null = null;
|
|
||||||
|
|
||||||
if (commonAncestor.nodeType === Node.TEXT_NODE) {
|
|
||||||
element = commonAncestor.parentElement;
|
|
||||||
} else {
|
|
||||||
element = commonAncestor as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the selected element has the same text color
|
|
||||||
while (element && element !== editorRef.current) {
|
|
||||||
const style = window.getComputedStyle(element);
|
|
||||||
const textColor = style.color;
|
|
||||||
if (textColor) {
|
|
||||||
// Convert to hex and compare
|
|
||||||
const colorToHex = (c: string): string | null => {
|
|
||||||
if (c.startsWith('#')) return c.toUpperCase();
|
|
||||||
const result = c.match(/\d+/g);
|
|
||||||
if (!result || result.length < 3) return null;
|
|
||||||
const r = result[0];
|
|
||||||
const g = result[1];
|
|
||||||
const b = result[2];
|
|
||||||
if (!r || !g || !b) return null;
|
|
||||||
const rHex = parseInt(r).toString(16).padStart(2, '0');
|
|
||||||
const gHex = parseInt(g).toString(16).padStart(2, '0');
|
|
||||||
const bHex = parseInt(b).toString(16).padStart(2, '0');
|
|
||||||
return `#${rHex}${gHex}${bHex}`.toUpperCase();
|
|
||||||
};
|
|
||||||
const hexTextColor = colorToHex(textColor);
|
|
||||||
// For black, also check if it's the default (no custom color)
|
|
||||||
if (color === '#000000') {
|
|
||||||
// If it's black and matches, or if no custom color is set (default black)
|
|
||||||
if (hexTextColor === '#000000' || !hexTextColor) {
|
|
||||||
isAlreadyApplied = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (hexTextColor && hexTextColor === color.toUpperCase()) {
|
|
||||||
isAlreadyApplied = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use foreColor command for text color
|
|
||||||
if (color === 'transparent' || color === 'default' || isAlreadyApplied) {
|
|
||||||
// Remove text color by removing format or setting to default
|
|
||||||
// If clicking black when it's already default, do nothing or reset
|
|
||||||
if (color === '#000000' && isAlreadyApplied) {
|
|
||||||
// Already default, no need to change
|
|
||||||
setTextColorOpen(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.execCommand('removeFormat', false);
|
|
||||||
setCurrentTextColor(null);
|
|
||||||
setCustomTextColor('#000000'); // Reset to default black
|
|
||||||
} else {
|
|
||||||
document.execCommand('foreColor', false, color);
|
|
||||||
setCurrentTextColor(color);
|
|
||||||
// Update custom text color input if it's a valid hex color
|
|
||||||
if (color !== 'transparent' && /^#[0-9A-Fa-f]{6}$/i.test(color)) {
|
|
||||||
setCustomTextColor(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update content
|
|
||||||
if (editorRef.current) {
|
|
||||||
onChange(editorRef.current.innerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close popover
|
|
||||||
setTextColorOpen(false);
|
|
||||||
|
|
||||||
// Check active formats after a short delay
|
|
||||||
setTimeout(checkActiveFormats, 10);
|
|
||||||
}, [isFocused, onChange, checkActiveFormats]);
|
|
||||||
|
|
||||||
// Handle input changes
|
// Handle input changes
|
||||||
const handleInput = React.useCallback(() => {
|
const handleInput = React.useCallback(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
@ -748,424 +387,6 @@ export function RichTextEditor({
|
|||||||
>
|
>
|
||||||
<Underline className="h-4 w-4" />
|
<Underline className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Highlight Color Picker */}
|
|
||||||
<Popover open={highlightColorOpen} onOpenChange={setHighlightColorOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
"h-7 w-7 p-0",
|
|
||||||
activeFormats.has('highlight') && "bg-blue-100 text-blue-700"
|
|
||||||
)}
|
|
||||||
title="Text Highlight"
|
|
||||||
>
|
|
||||||
<Highlighter className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-auto p-2"
|
|
||||||
align="start"
|
|
||||||
onPointerDownOutside={(e) => {
|
|
||||||
// Prevent closing when clicking inside popover
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('[data-popover-content]')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="space-y-2 relative" onClick={(e) => e.stopPropagation()}>
|
|
||||||
{/* Cancel Button */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute top-0 right-0 h-6 w-6 p-0 hover:bg-gray-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setHighlightColorOpen(false);
|
|
||||||
}}
|
|
||||||
title="Close"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 text-gray-500" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Highlight Color</div>
|
|
||||||
<div className="grid grid-cols-6 gap-1.5 pr-1 mb-2">
|
|
||||||
{HIGHLIGHT_COLORS.map((color) => {
|
|
||||||
const isActive = currentHighlightColor === color.value;
|
|
||||||
const isNoColor = color.value === 'transparent';
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={color.value}
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
// Standard toggle: if clicking the same color, remove it
|
|
||||||
if (isActive && !isNoColor) {
|
|
||||||
applyHighlight('transparent');
|
|
||||||
} else {
|
|
||||||
applyHighlight(color.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"w-6 h-6 rounded border-2 transition-all hover:scale-110 hover:shadow-md relative",
|
|
||||||
color.class,
|
|
||||||
isActive && "ring-2 ring-blue-600 ring-offset-1 border-blue-600",
|
|
||||||
isNoColor && "border-gray-400 bg-white",
|
|
||||||
!isNoColor && !isActive && "border-gray-300"
|
|
||||||
)}
|
|
||||||
title={isActive && !isNoColor ? `${color.name} (Click to remove)` : color.name}
|
|
||||||
style={!isNoColor ? { backgroundColor: color.value } : {}}
|
|
||||||
>
|
|
||||||
{isNoColor && (
|
|
||||||
<span className="text-[10px] text-gray-600 font-bold">×</span>
|
|
||||||
)}
|
|
||||||
{isActive && !isNoColor && (
|
|
||||||
<span className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="w-2 h-2 bg-white rounded-full shadow-sm"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remove Highlight Button - Standard pattern */}
|
|
||||||
{currentHighlightColor && currentHighlightColor !== 'transparent' && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-full h-7 text-xs"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
applyHighlight('transparent');
|
|
||||||
}}
|
|
||||||
title="Remove highlight color"
|
|
||||||
>
|
|
||||||
Remove Highlight
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Custom Color Picker */}
|
|
||||||
<div className="border-t border-gray-200 pt-2 mt-2">
|
|
||||||
<div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={currentHighlightColor && currentHighlightColor !== 'transparent' ? currentHighlightColor : customColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
const color = e.target.value;
|
|
||||||
setCustomColor(color);
|
|
||||||
// Only apply if there's selected text
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0 && !selection.getRangeAt(0).collapsed) {
|
|
||||||
applyHighlight(color);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-8 h-8 rounded border border-gray-300 cursor-pointer"
|
|
||||||
title="Pick a custom color"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentHighlightColor && currentHighlightColor !== 'transparent' ? currentHighlightColor : customColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const color = e.target.value;
|
|
||||||
// Allow typing freely - don't restrict input
|
|
||||||
setCustomColor(color);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Prevent popover from closing
|
|
||||||
e.stopPropagation();
|
|
||||||
// Allow all keys including backspace, delete, etc.
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
const color = e.currentTarget.value.trim();
|
|
||||||
// Apply if valid hex color
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/i.test(color)) {
|
|
||||||
applyHighlight(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onPaste={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// Get pasted text from clipboard
|
|
||||||
const pastedText = e.clipboardData.getData('text').trim();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Process after paste event completes
|
|
||||||
setTimeout(() => {
|
|
||||||
// Check if it's a valid hex color with #
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/i.test(pastedText)) {
|
|
||||||
setCustomColor(pastedText);
|
|
||||||
// Auto-apply if text is selected in editor
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0 && !selection.getRangeAt(0).collapsed) {
|
|
||||||
applyHighlight(pastedText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check if it's a hex color without # prefix (6 hex digits)
|
|
||||||
const hexMatch = pastedText.match(/^([0-9A-Fa-f]{6})$/);
|
|
||||||
if (hexMatch) {
|
|
||||||
const colorWithHash = `#${hexMatch[1]}`;
|
|
||||||
setCustomColor(colorWithHash);
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0 && !selection.getRangeAt(0).collapsed) {
|
|
||||||
applyHighlight(colorWithHash);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no match, allow default paste behavior (setTimeout allows default paste)
|
|
||||||
// Value will be updated by onChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
onFocus={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// Select all text on focus for easy replacement
|
|
||||||
e.target.select();
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// On blur, if valid color, apply it
|
|
||||||
const color = e.target.value.trim();
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/i.test(color)) {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0 && !selection.getRangeAt(0).collapsed) {
|
|
||||||
applyHighlight(color);
|
|
||||||
}
|
|
||||||
} else if (color === '') {
|
|
||||||
// Reset to current if cleared
|
|
||||||
setCustomColor(currentHighlightColor || '#FFEB3B');
|
|
||||||
} else {
|
|
||||||
// Keep the typed value even if invalid (user might be typing)
|
|
||||||
setCustomColor(color);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="#FFEB3B"
|
|
||||||
className="flex-1 h-7 px-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
||||||
title="Enter hex color code (Press Enter or click Apply). Supports copy-paste."
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck="false"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-2 text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
const color = customColor.trim();
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/i.test(color)) {
|
|
||||||
applyHighlight(color);
|
|
||||||
} else {
|
|
||||||
// If invalid, show current value or reset
|
|
||||||
setCustomColor(currentHighlightColor || '#FFEB3B');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Apply custom color"
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{/* Text Color Picker */}
|
|
||||||
<Popover open={textColorOpen} onOpenChange={setTextColorOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
"h-7 w-7 p-0",
|
|
||||||
activeFormats.has('textColor') && "bg-blue-100 text-blue-700"
|
|
||||||
)}
|
|
||||||
title="Text Color"
|
|
||||||
>
|
|
||||||
<Type className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-auto p-2"
|
|
||||||
align="start"
|
|
||||||
onPointerDownOutside={(e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('[data-popover-content]')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="space-y-2 relative" onClick={(e) => e.stopPropagation()}>
|
|
||||||
{/* Cancel Button */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute top-0 right-0 h-6 w-6 p-0 hover:bg-gray-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setTextColorOpen(false);
|
|
||||||
}}
|
|
||||||
title="Close"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 text-gray-500" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-xs font-semibold text-gray-700 mb-2 pr-6">Text Color</div>
|
|
||||||
<div className="grid grid-cols-6 gap-1.5 pr-1 mb-2">
|
|
||||||
{/* Default/Black Color Option - First position (standard) */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const isDefault = currentTextColor === '#000000' || (!currentTextColor && !activeFormats.has('textColor'));
|
|
||||||
// Toggle: if already default, do nothing (or could reset to default explicitly)
|
|
||||||
if (!isDefault) {
|
|
||||||
applyTextColor('#000000');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"w-6 h-6 rounded border-2 transition-all hover:scale-110 hover:shadow-md flex items-center justify-center bg-black",
|
|
||||||
(currentTextColor === '#000000' || (!currentTextColor && !activeFormats.has('textColor'))) && "ring-2 ring-blue-600 ring-offset-1 border-blue-600",
|
|
||||||
(currentTextColor !== '#000000' && (currentTextColor || activeFormats.has('textColor'))) && "border-gray-300"
|
|
||||||
)}
|
|
||||||
title="Default (Black)"
|
|
||||||
>
|
|
||||||
<span className="text-[10px] text-white font-bold">A</span>
|
|
||||||
{(currentTextColor === '#000000' || (!currentTextColor && !activeFormats.has('textColor'))) && (
|
|
||||||
<span className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="w-2 h-2 bg-white rounded-full shadow-sm"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{HIGHLIGHT_COLORS.filter(c => c.value !== 'transparent').map((color) => {
|
|
||||||
const isActive = currentTextColor === color.value;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={`text-${color.value}`}
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
// Standard toggle: if clicking the same color, reset to default
|
|
||||||
if (isActive) {
|
|
||||||
applyTextColor('#000000');
|
|
||||||
} else {
|
|
||||||
applyTextColor(color.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"w-6 h-6 rounded border-2 transition-all hover:scale-110 hover:shadow-md flex items-center justify-center relative",
|
|
||||||
isActive && "ring-2 ring-blue-600 ring-offset-1 border-blue-600",
|
|
||||||
!isActive && "border-gray-300"
|
|
||||||
)}
|
|
||||||
title={isActive ? `${color.name} (Click to reset to default)` : color.name}
|
|
||||||
style={{ color: color.value, borderColor: isActive ? '#2563eb' : color.value }}
|
|
||||||
>
|
|
||||||
<span className="text-xs font-bold">A</span>
|
|
||||||
{isActive && (
|
|
||||||
<span className="absolute inset-0 flex items-center justify-center">
|
|
||||||
<span className="w-2 h-2 bg-white rounded-full shadow-sm"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Remove Text Color Button - Standard pattern */}
|
|
||||||
{currentTextColor && currentTextColor !== '#000000' && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="w-full h-7 text-xs"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
applyTextColor('#000000');
|
|
||||||
}}
|
|
||||||
title="Reset to default black"
|
|
||||||
>
|
|
||||||
Reset to Default
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Custom Text Color Picker */}
|
|
||||||
<div className="border-t border-gray-200 pt-2 mt-2">
|
|
||||||
<div className="text-xs font-semibold text-gray-700 mb-1.5">Custom Color</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={currentTextColor || customTextColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
const color = e.target.value;
|
|
||||||
setCustomTextColor(color);
|
|
||||||
applyTextColor(color);
|
|
||||||
}}
|
|
||||||
className="w-8 h-8 rounded border border-gray-300 cursor-pointer"
|
|
||||||
title="Pick a custom text color"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={currentTextColor || customTextColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
const color = e.target.value;
|
|
||||||
setCustomTextColor(color);
|
|
||||||
// Don't auto-apply, let user press Apply or Enter
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
const color = e.currentTarget.value;
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
|
||||||
applyTextColor(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={(e) => {
|
|
||||||
// On blur, if valid color, apply it
|
|
||||||
const color = e.target.value;
|
|
||||||
if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
|
||||||
applyTextColor(color);
|
|
||||||
} else if (color === '') {
|
|
||||||
// Reset to current if cleared
|
|
||||||
setCustomTextColor(currentTextColor || '#000000');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="#000000"
|
|
||||||
className="flex-1 h-7 px-2 text-xs border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
||||||
title="Enter hex color code (Press Enter or click Apply)"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 px-2 text-xs"
|
|
||||||
onClick={() => applyTextColor(customTextColor)}
|
|
||||||
title="Apply custom text color"
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Headings */}
|
{/* Headings */}
|
||||||
@ -1328,8 +549,6 @@ export function RichTextEditor({
|
|||||||
"[&_strong]:font-bold",
|
"[&_strong]:font-bold",
|
||||||
"[&_em]:italic",
|
"[&_em]:italic",
|
||||||
"[&_u]:underline",
|
"[&_u]:underline",
|
||||||
// Highlight color styles - preserve background colors on spans
|
|
||||||
"[&_span[style*='background']]:px-0.5 [&_span[style*='background']]:rounded",
|
|
||||||
"[&_h1]:text-xl [&_h1]:font-bold [&_h1]:my-2",
|
"[&_h1]:text-xl [&_h1]:font-bold [&_h1]:my-2",
|
||||||
"[&_h2]:text-lg [&_h2]:font-semibold [&_h2]:my-2",
|
"[&_h2]:text-lg [&_h2]:font-semibold [&_h2]:my-2",
|
||||||
"[&_h3]:text-base [&_h3]:font-semibold [&_h3]:my-1",
|
"[&_h3]:text-base [&_h3]:font-semibold [&_h3]:my-1",
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Request Creation Component
|
|
||||||
*
|
|
||||||
* This component handles the creation of custom requests.
|
|
||||||
* Located in: src/custom/components/request-creation/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { CreateRequest } from '@/pages/CreateRequest/CreateRequest';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Request Overview Tab
|
|
||||||
*
|
|
||||||
* This component is specific to Custom requests.
|
|
||||||
* Located in: src/custom/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { OverviewTab } from '@/pages/RequestDetail/components/tabs/OverviewTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Request Workflow Tab
|
|
||||||
*
|
|
||||||
* This component is specific to Custom requests.
|
|
||||||
* Located in: src/custom/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { WorkflowTab } from '@/pages/RequestDetail/components/tabs/WorkflowTab';
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Request Flow
|
|
||||||
*
|
|
||||||
* This module exports all components, hooks, utilities, and types
|
|
||||||
* specific to Custom requests. This allows for complete segregation
|
|
||||||
* of custom request functionality.
|
|
||||||
*
|
|
||||||
* LOCATION: src/custom/
|
|
||||||
*
|
|
||||||
* To remove Custom flow completely:
|
|
||||||
* 1. Delete this entire folder: src/custom/
|
|
||||||
* 2. Remove from src/flows.ts registry
|
|
||||||
* 3. Done! All custom request code is removed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Request Detail Components
|
|
||||||
export { OverviewTab as CustomOverviewTab } from './components/request-detail/OverviewTab';
|
|
||||||
export { WorkflowTab as CustomWorkflowTab } from './components/request-detail/WorkflowTab';
|
|
||||||
|
|
||||||
// Request Creation Components
|
|
||||||
export { CreateRequest as CustomCreateRequest } from './components/request-creation/CreateRequest';
|
|
||||||
|
|
||||||
// Request Detail Screen (Complete standalone screen)
|
|
||||||
export { CustomRequestDetail } from './pages/RequestDetail';
|
|
||||||
|
|
||||||
// Re-export types
|
|
||||||
export type { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types';
|
|
||||||
@ -1,641 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Request Detail Screen
|
|
||||||
*
|
|
||||||
* Standalone, dedicated request detail screen for Custom requests.
|
|
||||||
* This is a complete module that uses custom request specific components.
|
|
||||||
*
|
|
||||||
* LOCATION: src/custom/pages/RequestDetail.tsx
|
|
||||||
*
|
|
||||||
* IMPORTANT: This entire file and all its dependencies are in src/custom/ folder.
|
|
||||||
* Deleting src/custom/ folder removes ALL custom request related code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import {
|
|
||||||
ClipboardList,
|
|
||||||
TrendingUp,
|
|
||||||
FileText,
|
|
||||||
Activity,
|
|
||||||
MessageSquare,
|
|
||||||
AlertTriangle,
|
|
||||||
FileCheck,
|
|
||||||
ShieldX,
|
|
||||||
RefreshCw,
|
|
||||||
ArrowLeft,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
// Context and hooks
|
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
|
||||||
import { useRequestDetails } from '@/hooks/useRequestDetails';
|
|
||||||
import { useRequestSocket } from '@/hooks/useRequestSocket';
|
|
||||||
import { useDocumentUpload } from '@/hooks/useDocumentUpload';
|
|
||||||
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
|
||||||
import { useModalManager } from '@/hooks/useModalManager';
|
|
||||||
import { downloadDocument } from '@/services/workflowApi';
|
|
||||||
|
|
||||||
// Custom Request Components (import from index to get properly aliased exports)
|
|
||||||
import { CustomOverviewTab, CustomWorkflowTab } from '../index';
|
|
||||||
|
|
||||||
// Shared Components (from src/shared/)
|
|
||||||
import { SharedComponents } from '@/shared/components';
|
|
||||||
const { DocumentsTab, ActivityTab, WorkNotesTab, SummaryTab, RequestDetailHeader, QuickActionsSidebar, RequestDetailModals } = SharedComponents;
|
|
||||||
|
|
||||||
// Other components
|
|
||||||
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
|
|
||||||
import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types';
|
|
||||||
import { PauseModal } from '@/components/workflow/PauseModal';
|
|
||||||
import { ResumeModal } from '@/components/workflow/ResumeModal';
|
|
||||||
import { RetriggerPauseModal } from '@/components/workflow/RetriggerPauseModal';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error Boundary Component
|
|
||||||
*/
|
|
||||||
class RequestDetailErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean; error: Error | null }> {
|
|
||||||
constructor(props: { children: ReactNode }) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
|
||||||
return { hasError: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
console.error('Custom RequestDetail Error:', error, errorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
|
||||||
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
||||||
<h2 className="text-2xl font-bold mb-2">Error Loading Request</h2>
|
|
||||||
<p className="text-gray-600 mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>
|
|
||||||
<Button onClick={() => window.location.reload()} className="mr-2">
|
|
||||||
Reload Page
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={() => window.history.back()}>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom RequestDetailInner Component
|
|
||||||
*/
|
|
||||||
function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests = [] }: RequestDetailProps) {
|
|
||||||
const params = useParams<{ requestId: string }>();
|
|
||||||
const requestIdentifier = params.requestId || propRequestId || '';
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const initialTab = urlParams.get('tab') || 'overview';
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
|
||||||
const [showShareSummaryModal, setShowShareSummaryModal] = useState(false);
|
|
||||||
const [summaryId, setSummaryId] = useState<string | null>(null);
|
|
||||||
const [summaryDetails, setSummaryDetails] = useState<SummaryDetails | null>(null);
|
|
||||||
const [loadingSummary, setLoadingSummary] = useState(false);
|
|
||||||
const [sharedRecipientsRefreshTrigger, setSharedRecipientsRefreshTrigger] = useState(0);
|
|
||||||
const [showPauseModal, setShowPauseModal] = useState(false);
|
|
||||||
const [showResumeModal, setShowResumeModal] = useState(false);
|
|
||||||
const [showRetriggerModal, setShowRetriggerModal] = useState(false);
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
// Custom hooks
|
|
||||||
const {
|
|
||||||
request,
|
|
||||||
apiRequest,
|
|
||||||
loading: requestLoading,
|
|
||||||
refreshing,
|
|
||||||
refreshDetails,
|
|
||||||
currentApprovalLevel,
|
|
||||||
isSpectator,
|
|
||||||
isInitiator,
|
|
||||||
existingParticipants,
|
|
||||||
accessDenied,
|
|
||||||
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
|
||||||
|
|
||||||
const {
|
|
||||||
mergedMessages,
|
|
||||||
unreadWorkNotes,
|
|
||||||
workNoteAttachments,
|
|
||||||
setWorkNoteAttachments,
|
|
||||||
} = useRequestSocket(requestIdentifier, apiRequest, activeTab, user);
|
|
||||||
|
|
||||||
const {
|
|
||||||
uploadingDocument,
|
|
||||||
triggerFileInput,
|
|
||||||
previewDocument,
|
|
||||||
setPreviewDocument,
|
|
||||||
documentPolicy,
|
|
||||||
documentError,
|
|
||||||
setDocumentError,
|
|
||||||
} = useDocumentUpload(apiRequest, refreshDetails);
|
|
||||||
|
|
||||||
const {
|
|
||||||
showApproveModal,
|
|
||||||
setShowApproveModal,
|
|
||||||
showRejectModal,
|
|
||||||
setShowRejectModal,
|
|
||||||
showAddApproverModal,
|
|
||||||
setShowAddApproverModal,
|
|
||||||
showAddSpectatorModal,
|
|
||||||
setShowAddSpectatorModal,
|
|
||||||
showSkipApproverModal,
|
|
||||||
setShowSkipApproverModal,
|
|
||||||
showActionStatusModal,
|
|
||||||
setShowActionStatusModal,
|
|
||||||
skipApproverData,
|
|
||||||
setSkipApproverData,
|
|
||||||
actionStatus,
|
|
||||||
setActionStatus,
|
|
||||||
handleApproveConfirm,
|
|
||||||
handleRejectConfirm,
|
|
||||||
handleAddApprover,
|
|
||||||
handleSkipApprover,
|
|
||||||
handleAddSpectator,
|
|
||||||
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails);
|
|
||||||
|
|
||||||
const {
|
|
||||||
conclusionRemark,
|
|
||||||
setConclusionRemark,
|
|
||||||
conclusionLoading,
|
|
||||||
conclusionSubmitting,
|
|
||||||
aiGenerated,
|
|
||||||
handleGenerateConclusion,
|
|
||||||
handleFinalizeConclusion,
|
|
||||||
} = useConclusionRemark(request, requestIdentifier, isInitiator, refreshDetails, onBack, setActionStatus, setShowActionStatusModal);
|
|
||||||
|
|
||||||
// Auto-switch tab when URL query parameter changes
|
|
||||||
useEffect(() => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const tabParam = urlParams.get('tab');
|
|
||||||
if (tabParam) {
|
|
||||||
setActiveTab(tabParam);
|
|
||||||
}
|
|
||||||
}, [requestIdentifier]);
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pause handlers
|
|
||||||
const handlePause = () => {
|
|
||||||
setShowPauseModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResume = () => {
|
|
||||||
setShowResumeModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResumeSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRetrigger = () => {
|
|
||||||
setShowRetriggerModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePauseSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRetriggerSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShareSummary = async () => {
|
|
||||||
if (!apiRequest?.requestId) {
|
|
||||||
toast.error('Request ID not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!summaryId) {
|
|
||||||
toast.error('Summary not available. Please ensure the request is closed and the summary has been generated.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowShareSummaryModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
|
||||||
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
|
||||||
|
|
||||||
// Fetch summary details if request is closed
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSummaryDetails = async () => {
|
|
||||||
if (!isClosed || !apiRequest?.requestId) {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadingSummary(true);
|
|
||||||
const summary = await getSummaryByRequestId(apiRequest.requestId);
|
|
||||||
|
|
||||||
if (summary?.summaryId) {
|
|
||||||
setSummaryId(summary.summaryId);
|
|
||||||
try {
|
|
||||||
const details = await getSummaryDetails(summary.summaryId);
|
|
||||||
setSummaryDetails(details);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to fetch summary details:', error);
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
} finally {
|
|
||||||
setLoadingSummary(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSummaryDetails();
|
|
||||||
}, [isClosed, apiRequest?.requestId]);
|
|
||||||
|
|
||||||
// Get current levels for WorkNotesTab
|
|
||||||
const 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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Loading state
|
|
||||||
if (requestLoading && !request && !apiRequest) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50" data-testid="loading-state">
|
|
||||||
<div className="text-center">
|
|
||||||
<RefreshCw className="w-12 h-12 text-blue-600 animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">Loading custom request details...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access Denied state
|
|
||||||
if (accessDenied?.denied) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="access-denied-state">
|
|
||||||
<div className="max-w-lg w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
||||||
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<ShieldX className="w-10 h-10 text-red-500" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-3">Access Denied</h2>
|
|
||||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
|
||||||
{accessDenied.message}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3 justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onBack || (() => window.history.back())}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Go to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not Found state
|
|
||||||
if (!request) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="not-found-state">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
||||||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<FileText className="w-10 h-10 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-3">Custom Request Not Found</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
The custom request you're looking for doesn't exist or may have been deleted.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3 justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onBack || (() => window.history.back())}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Go to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="min-h-screen bg-gray-50" data-testid="custom-request-detail-page">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
{/* Header Section */}
|
|
||||||
<RequestDetailHeader
|
|
||||||
request={request}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onBack={onBack || (() => window.history.back())}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
onShareSummary={handleShareSummary}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="custom-request-detail-tabs">
|
|
||||||
<div className="mb-4 sm:mb-6">
|
|
||||||
<TabsList className="grid grid-cols-3 sm:grid-cols-6 lg:flex lg:flex-row h-auto bg-gray-100 p-1.5 sm:p-1 rounded-lg gap-1.5 sm:gap-1">
|
|
||||||
<TabsTrigger
|
|
||||||
value="overview"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-overview"
|
|
||||||
>
|
|
||||||
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Overview</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
{isClosed && summaryDetails && (
|
|
||||||
<TabsTrigger
|
|
||||||
value="summary"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-summary"
|
|
||||||
>
|
|
||||||
<FileCheck className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Summary</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger
|
|
||||||
value="workflow"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-workflow"
|
|
||||||
>
|
|
||||||
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Workflow</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="documents"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-documents"
|
|
||||||
>
|
|
||||||
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Docs</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="activity"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 col-span-1 sm:col-span-1"
|
|
||||||
data-testid="tab-activity"
|
|
||||||
>
|
|
||||||
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Activity</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="worknotes"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 relative col-span-2 sm:col-span-1"
|
|
||||||
data-testid="tab-worknotes"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Work Notes</span>
|
|
||||||
{unreadWorkNotes > 0 && (
|
|
||||||
<Badge
|
|
||||||
className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0"
|
|
||||||
data-testid="worknotes-unread-badge"
|
|
||||||
>
|
|
||||||
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Layout */}
|
|
||||||
<div className={activeTab === 'worknotes' ? '' : 'grid grid-cols-1 lg:grid-cols-3 gap-6'}>
|
|
||||||
{/* Left Column: Tab content */}
|
|
||||||
<div className={activeTab === 'worknotes' ? '' : 'lg:col-span-2'}>
|
|
||||||
<TabsContent value="overview" className="mt-0" data-testid="overview-tab-content">
|
|
||||||
<CustomOverviewTab
|
|
||||||
request={request}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
needsClosure={needsClosure}
|
|
||||||
conclusionRemark={conclusionRemark}
|
|
||||||
setConclusionRemark={setConclusionRemark}
|
|
||||||
conclusionLoading={conclusionLoading}
|
|
||||||
conclusionSubmitting={conclusionSubmitting}
|
|
||||||
aiGenerated={aiGenerated}
|
|
||||||
handleGenerateConclusion={handleGenerateConclusion}
|
|
||||||
handleFinalizeConclusion={handleFinalizeConclusion}
|
|
||||||
onPause={handlePause}
|
|
||||||
onResume={handleResume}
|
|
||||||
onRetrigger={handleRetrigger}
|
|
||||||
currentUserIsApprover={!!currentApprovalLevel}
|
|
||||||
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
|
||||||
currentUserId={(user as any)?.userId}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{isClosed && (
|
|
||||||
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
|
|
||||||
<SummaryTab
|
|
||||||
summary={summaryDetails}
|
|
||||||
loading={loadingSummary}
|
|
||||||
onShare={handleShareSummary}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsContent value="workflow" className="mt-0">
|
|
||||||
<CustomWorkflowTab
|
|
||||||
request={request}
|
|
||||||
user={user}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
onSkipApprover={(data) => {
|
|
||||||
if (!data.levelId) {
|
|
||||||
alert('Level ID not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSkipApproverData(data);
|
|
||||||
setShowSkipApproverModal(true);
|
|
||||||
}}
|
|
||||||
onRefresh={refreshDetails}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="documents" className="mt-0">
|
|
||||||
<DocumentsTab
|
|
||||||
request={request}
|
|
||||||
workNoteAttachments={workNoteAttachments}
|
|
||||||
uploadingDocument={uploadingDocument}
|
|
||||||
documentPolicy={documentPolicy}
|
|
||||||
triggerFileInput={triggerFileInput}
|
|
||||||
setPreviewDocument={setPreviewDocument}
|
|
||||||
downloadDocument={downloadDocument}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="activity" className="mt-0">
|
|
||||||
<ActivityTab request={request} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="worknotes" className="mt-0" forceMount={true} hidden={activeTab !== 'worknotes'}>
|
|
||||||
<WorkNotesTab
|
|
||||||
requestId={requestIdentifier}
|
|
||||||
requestTitle={request.title}
|
|
||||||
mergedMessages={mergedMessages}
|
|
||||||
setWorkNoteAttachments={setWorkNoteAttachments}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
isSpectator={isSpectator}
|
|
||||||
currentLevels={currentLevels}
|
|
||||||
onAddApprover={handleAddApprover}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Quick Actions Sidebar */}
|
|
||||||
{activeTab !== 'worknotes' && (
|
|
||||||
<QuickActionsSidebar
|
|
||||||
request={request}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
isSpectator={isSpectator}
|
|
||||||
currentApprovalLevel={currentApprovalLevel}
|
|
||||||
onAddApprover={() => setShowAddApproverModal(true)}
|
|
||||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
|
||||||
onApprove={() => setShowApproveModal(true)}
|
|
||||||
onReject={() => setShowRejectModal(true)}
|
|
||||||
onPause={handlePause}
|
|
||||||
onResume={handleResume}
|
|
||||||
onRetrigger={handleRetrigger}
|
|
||||||
summaryId={summaryId}
|
|
||||||
refreshTrigger={sharedRecipientsRefreshTrigger}
|
|
||||||
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
|
||||||
currentUserId={(user as any)?.userId}
|
|
||||||
apiRequest={apiRequest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share Summary Modal */}
|
|
||||||
{showShareSummaryModal && summaryId && (
|
|
||||||
<ShareSummaryModal
|
|
||||||
isOpen={showShareSummaryModal}
|
|
||||||
onClose={() => setShowShareSummaryModal(false)}
|
|
||||||
summaryId={summaryId}
|
|
||||||
requestTitle={request?.title || 'N/A'}
|
|
||||||
onSuccess={() => {
|
|
||||||
refreshDetails();
|
|
||||||
setSharedRecipientsRefreshTrigger(prev => prev + 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pause Modals */}
|
|
||||||
{showPauseModal && apiRequest?.requestId && (
|
|
||||||
<PauseModal
|
|
||||||
isOpen={showPauseModal}
|
|
||||||
onClose={() => setShowPauseModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
levelId={currentApprovalLevel?.levelId || null}
|
|
||||||
onSuccess={handlePauseSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showResumeModal && apiRequest?.requestId && (
|
|
||||||
<ResumeModal
|
|
||||||
isOpen={showResumeModal}
|
|
||||||
onClose={() => setShowResumeModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
onSuccess={handleResumeSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showRetriggerModal && apiRequest?.requestId && (
|
|
||||||
<RetriggerPauseModal
|
|
||||||
isOpen={showRetriggerModal}
|
|
||||||
onClose={() => setShowRetriggerModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
approverName={request?.pauseInfo?.pausedBy?.name}
|
|
||||||
onSuccess={handleRetriggerSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
<RequestDetailModals
|
|
||||||
showApproveModal={showApproveModal}
|
|
||||||
showRejectModal={showRejectModal}
|
|
||||||
showAddApproverModal={showAddApproverModal}
|
|
||||||
showAddSpectatorModal={showAddSpectatorModal}
|
|
||||||
showSkipApproverModal={showSkipApproverModal}
|
|
||||||
showActionStatusModal={showActionStatusModal}
|
|
||||||
previewDocument={previewDocument}
|
|
||||||
documentError={documentError}
|
|
||||||
request={request}
|
|
||||||
skipApproverData={skipApproverData}
|
|
||||||
actionStatus={actionStatus}
|
|
||||||
existingParticipants={existingParticipants}
|
|
||||||
currentLevels={currentLevels}
|
|
||||||
setShowApproveModal={setShowApproveModal}
|
|
||||||
setShowRejectModal={setShowRejectModal}
|
|
||||||
setShowAddApproverModal={setShowAddApproverModal}
|
|
||||||
setShowAddSpectatorModal={setShowAddSpectatorModal}
|
|
||||||
setShowSkipApproverModal={setShowSkipApproverModal}
|
|
||||||
setShowActionStatusModal={setShowActionStatusModal}
|
|
||||||
setPreviewDocument={setPreviewDocument}
|
|
||||||
setDocumentError={setDocumentError}
|
|
||||||
setSkipApproverData={setSkipApproverData}
|
|
||||||
setActionStatus={setActionStatus}
|
|
||||||
handleApproveConfirm={handleApproveConfirm}
|
|
||||||
handleRejectConfirm={handleRejectConfirm}
|
|
||||||
handleAddApprover={handleAddApprover}
|
|
||||||
handleAddSpectator={handleAddSpectator}
|
|
||||||
handleSkipApprover={handleSkipApprover}
|
|
||||||
downloadDocument={downloadDocument}
|
|
||||||
documentPolicy={documentPolicy}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom RequestDetail Component (Exported)
|
|
||||||
*/
|
|
||||||
export function CustomRequestDetail(props: RequestDetailProps) {
|
|
||||||
return (
|
|
||||||
<RequestDetailErrorBoundary>
|
|
||||||
<CustomRequestDetailInner {...props} />
|
|
||||||
</RequestDetailErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Creation Wizard
|
|
||||||
*
|
|
||||||
* This component handles the creation of dealer claim requests.
|
|
||||||
* Located in: src/dealer-claim/components/request-creation/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { ClaimManagementWizard } from '@/components/workflow/ClaimManagementWizard';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim IO Tab
|
|
||||||
*
|
|
||||||
* This component handles IO (Internal Order) management for dealer claims.
|
|
||||||
* Located in: src/dealer-claim/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { IOTab } from '@/pages/RequestDetail/components/tabs/IOTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Overview Tab
|
|
||||||
*
|
|
||||||
* This component is specific to Dealer Claim requests.
|
|
||||||
* Located in: src/dealer-claim/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { ClaimManagementOverviewTab as DealerClaimOverviewTab } from '@/pages/RequestDetail/components/tabs/ClaimManagementOverviewTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Workflow Tab
|
|
||||||
*
|
|
||||||
* This component is specific to Dealer Claim requests.
|
|
||||||
* Located in: src/dealer-claim/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { DealerClaimWorkflowTab } from '@/pages/RequestDetail/components/tabs/DealerClaimWorkflowTab';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Detail Cards
|
|
||||||
*
|
|
||||||
* These components are specific to Dealer Claim request details.
|
|
||||||
* Located in: src/dealer-claim/components/request-detail/claim-cards/
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ActivityInformationCard } from '@/pages/RequestDetail/components/claim-cards/ActivityInformationCard';
|
|
||||||
export { DealerInformationCard } from '@/pages/RequestDetail/components/claim-cards/DealerInformationCard';
|
|
||||||
export { ProcessDetailsCard } from '@/pages/RequestDetail/components/claim-cards/ProcessDetailsCard';
|
|
||||||
export { ProposalDetailsCard } from '@/pages/RequestDetail/components/claim-cards/ProposalDetailsCard';
|
|
||||||
export { RequestInitiatorCard } from '@/pages/RequestDetail/components/claim-cards/RequestInitiatorCard';
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Detail Modals
|
|
||||||
*
|
|
||||||
* These modals are specific to Dealer Claim request details.
|
|
||||||
* Located in: src/dealer-claim/components/request-detail/modals/
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { CreditNoteSAPModal } from '@/pages/RequestDetail/components/modals/CreditNoteSAPModal';
|
|
||||||
export { DealerCompletionDocumentsModal } from '@/pages/RequestDetail/components/modals/DealerCompletionDocumentsModal';
|
|
||||||
export { DealerProposalSubmissionModal } from '@/pages/RequestDetail/components/modals/DealerProposalSubmissionModal';
|
|
||||||
export { DeptLeadIOApprovalModal } from '@/pages/RequestDetail/components/modals/DeptLeadIOApprovalModal';
|
|
||||||
export { EditClaimAmountModal } from '@/pages/RequestDetail/components/modals/EditClaimAmountModal';
|
|
||||||
export { EmailNotificationTemplateModal } from '@/pages/RequestDetail/components/modals/EmailNotificationTemplateModal';
|
|
||||||
export { InitiatorProposalApprovalModal } from '@/pages/RequestDetail/components/modals/InitiatorProposalApprovalModal';
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Flow
|
|
||||||
*
|
|
||||||
* This module exports all components, hooks, utilities, and types
|
|
||||||
* specific to Dealer Claim requests. This allows for complete segregation
|
|
||||||
* of dealer claim functionality.
|
|
||||||
*
|
|
||||||
* LOCATION: src/dealer-claim/
|
|
||||||
*
|
|
||||||
* To remove Dealer Claim flow completely:
|
|
||||||
* 1. Delete this entire folder: src/dealer-claim/
|
|
||||||
* 2. Remove from src/flows.ts registry
|
|
||||||
* 3. Done! All dealer claim code is removed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Request Detail Components
|
|
||||||
export { DealerClaimOverviewTab } from './components/request-detail/OverviewTab';
|
|
||||||
export { DealerClaimWorkflowTab } from './components/request-detail/WorkflowTab';
|
|
||||||
export { IOTab } from './components/request-detail/IOTab';
|
|
||||||
|
|
||||||
// Request Detail Cards
|
|
||||||
export * from './components/request-detail/claim-cards';
|
|
||||||
|
|
||||||
// Request Detail Modals
|
|
||||||
export * from './components/request-detail/modals';
|
|
||||||
|
|
||||||
// Request Creation Components
|
|
||||||
export { ClaimManagementWizard } from './components/request-creation/ClaimManagementWizard';
|
|
||||||
|
|
||||||
// Request Detail Screen (Complete standalone screen)
|
|
||||||
export { DealerClaimRequestDetail } from './pages/RequestDetail';
|
|
||||||
|
|
||||||
// Re-export types
|
|
||||||
export type { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types';
|
|
||||||
@ -1,674 +0,0 @@
|
|||||||
/**
|
|
||||||
* Dealer Claim Request Detail Screen
|
|
||||||
*
|
|
||||||
* Standalone, dedicated request detail screen for Dealer Claim requests.
|
|
||||||
* This is a complete module that uses dealer claim specific components.
|
|
||||||
*
|
|
||||||
* LOCATION: src/dealer-claim/pages/RequestDetail.tsx
|
|
||||||
*
|
|
||||||
* IMPORTANT: This entire file and all its dependencies are in src/dealer-claim/ folder.
|
|
||||||
* Deleting src/dealer-claim/ folder removes ALL dealer claim related code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
||||||
import {
|
|
||||||
ClipboardList,
|
|
||||||
TrendingUp,
|
|
||||||
FileText,
|
|
||||||
Activity,
|
|
||||||
MessageSquare,
|
|
||||||
AlertTriangle,
|
|
||||||
FileCheck,
|
|
||||||
ShieldX,
|
|
||||||
RefreshCw,
|
|
||||||
ArrowLeft,
|
|
||||||
DollarSign,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
|
|
||||||
// Context and hooks
|
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
|
||||||
import { useRequestDetails } from '@/hooks/useRequestDetails';
|
|
||||||
import { useRequestSocket } from '@/hooks/useRequestSocket';
|
|
||||||
import { useDocumentUpload } from '@/hooks/useDocumentUpload';
|
|
||||||
import { useModalManager } from '@/hooks/useModalManager';
|
|
||||||
import { downloadDocument } from '@/services/workflowApi';
|
|
||||||
|
|
||||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
|
||||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
|
||||||
|
|
||||||
// Shared Components
|
|
||||||
import { SharedComponents } from '@/shared/components';
|
|
||||||
const { DocumentsTab, ActivityTab, WorkNotesTab, SummaryTab, RequestDetailHeader, QuickActionsSidebar, RequestDetailModals } = SharedComponents;
|
|
||||||
|
|
||||||
// Other components
|
|
||||||
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
|
|
||||||
import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types';
|
|
||||||
import { PauseModal } from '@/components/workflow/PauseModal';
|
|
||||||
import { ResumeModal } from '@/components/workflow/ResumeModal';
|
|
||||||
import { RetriggerPauseModal } from '@/components/workflow/RetriggerPauseModal';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error Boundary Component
|
|
||||||
*/
|
|
||||||
class RequestDetailErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean; error: Error | null }> {
|
|
||||||
constructor(props: { children: ReactNode }) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false, error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: Error) {
|
|
||||||
return { hasError: true, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
||||||
console.error('Dealer Claim RequestDetail Error:', error, errorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
|
||||||
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
||||||
<h2 className="text-2xl font-bold mb-2">Error Loading Request</h2>
|
|
||||||
<p className="text-gray-600 mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>
|
|
||||||
<Button onClick={() => window.location.reload()} className="mr-2">
|
|
||||||
Reload Page
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={() => window.history.back()}>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dealer Claim RequestDetailInner Component
|
|
||||||
*/
|
|
||||||
function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests = [] }: RequestDetailProps) {
|
|
||||||
const params = useParams<{ requestId: string }>();
|
|
||||||
const requestIdentifier = params.requestId || propRequestId || '';
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const initialTab = urlParams.get('tab') || 'overview';
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
|
||||||
const [showShareSummaryModal, setShowShareSummaryModal] = useState(false);
|
|
||||||
const [summaryId, setSummaryId] = useState<string | null>(null);
|
|
||||||
const [summaryDetails, setSummaryDetails] = useState<SummaryDetails | null>(null);
|
|
||||||
const [loadingSummary, setLoadingSummary] = useState(false);
|
|
||||||
const [sharedRecipientsRefreshTrigger, setSharedRecipientsRefreshTrigger] = useState(0);
|
|
||||||
const [showPauseModal, setShowPauseModal] = useState(false);
|
|
||||||
const [showResumeModal, setShowResumeModal] = useState(false);
|
|
||||||
const [showRetriggerModal, setShowRetriggerModal] = useState(false);
|
|
||||||
const { user } = useAuth();
|
|
||||||
|
|
||||||
// Custom hooks
|
|
||||||
const {
|
|
||||||
request,
|
|
||||||
apiRequest,
|
|
||||||
loading: requestLoading,
|
|
||||||
refreshing,
|
|
||||||
refreshDetails,
|
|
||||||
currentApprovalLevel,
|
|
||||||
isSpectator,
|
|
||||||
isInitiator,
|
|
||||||
existingParticipants,
|
|
||||||
accessDenied,
|
|
||||||
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
|
||||||
|
|
||||||
// Determine if user is initiator
|
|
||||||
const currentUserId = (user as any)?.userId || '';
|
|
||||||
const currentUserEmail = (user as any)?.email?.toLowerCase() || '';
|
|
||||||
const initiatorUserId = apiRequest?.initiator?.userId;
|
|
||||||
const initiatorEmail = apiRequest?.initiator?.email?.toLowerCase();
|
|
||||||
const isUserInitiator = apiRequest?.initiator && (
|
|
||||||
(initiatorUserId && initiatorUserId === currentUserId) ||
|
|
||||||
(initiatorEmail && initiatorEmail === currentUserEmail)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Determine if user is department lead (whoever is in step 3 / approval level 3)
|
|
||||||
const approvalLevels = apiRequest?.approvalLevels || [];
|
|
||||||
const step3Level = approvalLevels.find((level: any) =>
|
|
||||||
(level.levelNumber || level.level_number) === 3
|
|
||||||
);
|
|
||||||
const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
|
|
||||||
const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver?.email || step3Level?.approverEmail || '').toLowerCase().trim();
|
|
||||||
|
|
||||||
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
|
||||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
|
||||||
|
|
||||||
const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
|
|
||||||
const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
|
|
||||||
step3Status === 'IN_PROGRESS';
|
|
||||||
|
|
||||||
const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || 0;
|
|
||||||
const isStep3CurrentLevel = currentLevel === 3;
|
|
||||||
|
|
||||||
const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
|
|
||||||
(deptLeadUserId && deptLeadUserId === currentUserId) ||
|
|
||||||
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
|
|
||||||
);
|
|
||||||
|
|
||||||
// IO tab visibility for dealer claims
|
|
||||||
const showIOTab = isUserInitiator || isDeptLead || isStep3CurrentApprover;
|
|
||||||
|
|
||||||
const {
|
|
||||||
mergedMessages,
|
|
||||||
unreadWorkNotes,
|
|
||||||
workNoteAttachments,
|
|
||||||
setWorkNoteAttachments,
|
|
||||||
} = useRequestSocket(requestIdentifier, apiRequest, activeTab, user);
|
|
||||||
|
|
||||||
const {
|
|
||||||
uploadingDocument,
|
|
||||||
triggerFileInput,
|
|
||||||
previewDocument,
|
|
||||||
setPreviewDocument,
|
|
||||||
documentPolicy,
|
|
||||||
documentError,
|
|
||||||
setDocumentError,
|
|
||||||
} = useDocumentUpload(apiRequest, refreshDetails);
|
|
||||||
|
|
||||||
const {
|
|
||||||
showApproveModal,
|
|
||||||
setShowApproveModal,
|
|
||||||
showRejectModal,
|
|
||||||
setShowRejectModal,
|
|
||||||
showAddApproverModal,
|
|
||||||
setShowAddApproverModal,
|
|
||||||
showAddSpectatorModal,
|
|
||||||
setShowAddSpectatorModal,
|
|
||||||
showSkipApproverModal,
|
|
||||||
setShowSkipApproverModal,
|
|
||||||
showActionStatusModal,
|
|
||||||
setShowActionStatusModal,
|
|
||||||
skipApproverData,
|
|
||||||
setSkipApproverData,
|
|
||||||
actionStatus,
|
|
||||||
setActionStatus,
|
|
||||||
handleApproveConfirm,
|
|
||||||
handleRejectConfirm,
|
|
||||||
handleAddApprover,
|
|
||||||
handleSkipApprover,
|
|
||||||
handleAddSpectator,
|
|
||||||
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails);
|
|
||||||
|
|
||||||
// Auto-switch tab when URL query parameter changes
|
|
||||||
useEffect(() => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const tabParam = urlParams.get('tab');
|
|
||||||
if (tabParam) {
|
|
||||||
setActiveTab(tabParam);
|
|
||||||
}
|
|
||||||
}, [requestIdentifier]);
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pause handlers
|
|
||||||
const handlePause = () => {
|
|
||||||
setShowPauseModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResume = () => {
|
|
||||||
setShowResumeModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResumeSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRetrigger = () => {
|
|
||||||
setShowRetriggerModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePauseSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRetriggerSuccess = async () => {
|
|
||||||
await refreshDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShareSummary = async () => {
|
|
||||||
if (!apiRequest?.requestId) {
|
|
||||||
toast.error('Request ID not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!summaryId) {
|
|
||||||
toast.error('Summary not available. Please ensure the request is closed and the summary has been generated.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowShareSummaryModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
|
||||||
|
|
||||||
// Fetch summary details if request is closed
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSummaryDetails = async () => {
|
|
||||||
if (!isClosed || !apiRequest?.requestId) {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoadingSummary(true);
|
|
||||||
const summary = await getSummaryByRequestId(apiRequest.requestId);
|
|
||||||
|
|
||||||
if (summary?.summaryId) {
|
|
||||||
setSummaryId(summary.summaryId);
|
|
||||||
try {
|
|
||||||
const details = await getSummaryDetails(summary.summaryId);
|
|
||||||
setSummaryDetails(details);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to fetch summary details:', error);
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
setSummaryDetails(null);
|
|
||||||
setSummaryId(null);
|
|
||||||
} finally {
|
|
||||||
setLoadingSummary(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSummaryDetails();
|
|
||||||
}, [isClosed, apiRequest?.requestId]);
|
|
||||||
|
|
||||||
// Get current levels for WorkNotesTab
|
|
||||||
const 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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Loading state
|
|
||||||
if (requestLoading && !request && !apiRequest) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50" data-testid="loading-state">
|
|
||||||
<div className="text-center">
|
|
||||||
<RefreshCw className="w-12 h-12 text-blue-600 animate-spin mx-auto mb-4" />
|
|
||||||
<p className="text-gray-600">Loading dealer claim request details...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Access Denied state
|
|
||||||
if (accessDenied?.denied) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="access-denied-state">
|
|
||||||
<div className="max-w-lg w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
||||||
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<ShieldX className="w-10 h-10 text-red-500" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-3">Access Denied</h2>
|
|
||||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
|
||||||
{accessDenied.message}
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3 justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onBack || (() => window.history.back())}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Go to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not Found state
|
|
||||||
if (!request) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="not-found-state">
|
|
||||||
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
|
||||||
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<FileText className="w-10 h-10 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 mb-3">Dealer Claim Request Not Found</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
The dealer claim request you're looking for doesn't exist or may have been deleted.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-3 justify-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onBack || (() => window.history.back())}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Go to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="min-h-screen bg-gray-50" data-testid="dealer-claim-request-detail-page">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
{/* Header Section */}
|
|
||||||
<RequestDetailHeader
|
|
||||||
request={request}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onBack={onBack || (() => window.history.back())}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
onShareSummary={handleShareSummary}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="dealer-claim-request-detail-tabs">
|
|
||||||
<div className="mb-4 sm:mb-6">
|
|
||||||
<TabsList className="grid grid-cols-3 sm:grid-cols-6 lg:flex lg:flex-row h-auto bg-gray-100 p-1.5 sm:p-1 rounded-lg gap-1.5 sm:gap-1">
|
|
||||||
<TabsTrigger
|
|
||||||
value="overview"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-overview"
|
|
||||||
>
|
|
||||||
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Overview</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
{isClosed && summaryDetails && (
|
|
||||||
<TabsTrigger
|
|
||||||
value="summary"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-summary"
|
|
||||||
>
|
|
||||||
<FileCheck className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Summary</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger
|
|
||||||
value="workflow"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-workflow"
|
|
||||||
>
|
|
||||||
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Workflow</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
{showIOTab && (
|
|
||||||
<TabsTrigger
|
|
||||||
value="io"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-io"
|
|
||||||
>
|
|
||||||
<DollarSign className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">IO</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger
|
|
||||||
value="documents"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
|
||||||
data-testid="tab-documents"
|
|
||||||
>
|
|
||||||
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Docs</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="activity"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 col-span-1 sm:col-span-1"
|
|
||||||
data-testid="tab-activity"
|
|
||||||
>
|
|
||||||
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Activity</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="worknotes"
|
|
||||||
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 relative col-span-2 sm:col-span-1"
|
|
||||||
data-testid="tab-worknotes"
|
|
||||||
>
|
|
||||||
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
|
||||||
<span className="truncate">Work Notes</span>
|
|
||||||
{unreadWorkNotes > 0 && (
|
|
||||||
<Badge
|
|
||||||
className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0"
|
|
||||||
data-testid="worknotes-unread-badge"
|
|
||||||
>
|
|
||||||
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Layout */}
|
|
||||||
<div className={activeTab === 'worknotes' ? '' : 'grid grid-cols-1 lg:grid-cols-3 gap-6'}>
|
|
||||||
{/* Left Column: Tab content */}
|
|
||||||
<div className={activeTab === 'worknotes' ? '' : 'lg:col-span-2'}>
|
|
||||||
<TabsContent value="overview" className="mt-0" data-testid="overview-tab-content">
|
|
||||||
<DealerClaimOverviewTab
|
|
||||||
request={request}
|
|
||||||
apiRequest={apiRequest}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{isClosed && (
|
|
||||||
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
|
|
||||||
<SummaryTab
|
|
||||||
summary={summaryDetails}
|
|
||||||
loading={loadingSummary}
|
|
||||||
onShare={handleShareSummary}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsContent value="workflow" className="mt-0">
|
|
||||||
<DealerClaimWorkflowTab
|
|
||||||
request={request}
|
|
||||||
user={user}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
onSkipApprover={(data) => {
|
|
||||||
if (!data.levelId) {
|
|
||||||
alert('Level ID not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSkipApproverData(data);
|
|
||||||
setShowSkipApproverModal(true);
|
|
||||||
}}
|
|
||||||
onRefresh={refreshDetails}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{showIOTab && (
|
|
||||||
<TabsContent value="io" className="mt-0">
|
|
||||||
<IOTab
|
|
||||||
request={request}
|
|
||||||
apiRequest={apiRequest}
|
|
||||||
onRefresh={refreshDetails}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsContent value="documents" className="mt-0">
|
|
||||||
<DocumentsTab
|
|
||||||
request={request}
|
|
||||||
workNoteAttachments={workNoteAttachments}
|
|
||||||
uploadingDocument={uploadingDocument}
|
|
||||||
documentPolicy={documentPolicy}
|
|
||||||
triggerFileInput={triggerFileInput}
|
|
||||||
setPreviewDocument={setPreviewDocument}
|
|
||||||
downloadDocument={downloadDocument}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="activity" className="mt-0">
|
|
||||||
<ActivityTab request={request} />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="worknotes" className="mt-0" forceMount={true} hidden={activeTab !== 'worknotes'}>
|
|
||||||
<WorkNotesTab
|
|
||||||
requestId={requestIdentifier}
|
|
||||||
requestTitle={request.title}
|
|
||||||
mergedMessages={mergedMessages}
|
|
||||||
setWorkNoteAttachments={setWorkNoteAttachments}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
isSpectator={isSpectator}
|
|
||||||
currentLevels={currentLevels}
|
|
||||||
onAddApprover={handleAddApprover}
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Column: Quick Actions Sidebar */}
|
|
||||||
{activeTab !== 'worknotes' && (
|
|
||||||
<QuickActionsSidebar
|
|
||||||
request={request}
|
|
||||||
isInitiator={isInitiator}
|
|
||||||
isSpectator={isSpectator}
|
|
||||||
currentApprovalLevel={currentApprovalLevel}
|
|
||||||
onAddApprover={() => setShowAddApproverModal(true)}
|
|
||||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
|
||||||
onApprove={() => setShowApproveModal(true)}
|
|
||||||
onReject={() => setShowRejectModal(true)}
|
|
||||||
onPause={handlePause}
|
|
||||||
onResume={handleResume}
|
|
||||||
onRetrigger={handleRetrigger}
|
|
||||||
summaryId={summaryId}
|
|
||||||
refreshTrigger={sharedRecipientsRefreshTrigger}
|
|
||||||
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
|
||||||
currentUserId={currentUserId}
|
|
||||||
apiRequest={apiRequest}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Share Summary Modal */}
|
|
||||||
{showShareSummaryModal && summaryId && (
|
|
||||||
<ShareSummaryModal
|
|
||||||
isOpen={showShareSummaryModal}
|
|
||||||
onClose={() => setShowShareSummaryModal(false)}
|
|
||||||
summaryId={summaryId}
|
|
||||||
requestTitle={request?.title || 'N/A'}
|
|
||||||
onSuccess={() => {
|
|
||||||
refreshDetails();
|
|
||||||
setSharedRecipientsRefreshTrigger(prev => prev + 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pause Modals */}
|
|
||||||
{showPauseModal && apiRequest?.requestId && (
|
|
||||||
<PauseModal
|
|
||||||
isOpen={showPauseModal}
|
|
||||||
onClose={() => setShowPauseModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
levelId={currentApprovalLevel?.levelId || null}
|
|
||||||
onSuccess={handlePauseSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showResumeModal && apiRequest?.requestId && (
|
|
||||||
<ResumeModal
|
|
||||||
isOpen={showResumeModal}
|
|
||||||
onClose={() => setShowResumeModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
onSuccess={handleResumeSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showRetriggerModal && apiRequest?.requestId && (
|
|
||||||
<RetriggerPauseModal
|
|
||||||
isOpen={showRetriggerModal}
|
|
||||||
onClose={() => setShowRetriggerModal(false)}
|
|
||||||
requestId={apiRequest.requestId}
|
|
||||||
approverName={request?.pauseInfo?.pausedBy?.name}
|
|
||||||
onSuccess={handleRetriggerSuccess}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Modals */}
|
|
||||||
<RequestDetailModals
|
|
||||||
showApproveModal={showApproveModal}
|
|
||||||
showRejectModal={showRejectModal}
|
|
||||||
showAddApproverModal={showAddApproverModal}
|
|
||||||
showAddSpectatorModal={showAddSpectatorModal}
|
|
||||||
showSkipApproverModal={showSkipApproverModal}
|
|
||||||
showActionStatusModal={showActionStatusModal}
|
|
||||||
previewDocument={previewDocument}
|
|
||||||
documentError={documentError}
|
|
||||||
request={request}
|
|
||||||
skipApproverData={skipApproverData}
|
|
||||||
actionStatus={actionStatus}
|
|
||||||
existingParticipants={existingParticipants}
|
|
||||||
currentLevels={currentLevels}
|
|
||||||
setShowApproveModal={setShowApproveModal}
|
|
||||||
setShowRejectModal={setShowRejectModal}
|
|
||||||
setShowAddApproverModal={setShowAddApproverModal}
|
|
||||||
setShowAddSpectatorModal={setShowAddSpectatorModal}
|
|
||||||
setShowSkipApproverModal={setShowSkipApproverModal}
|
|
||||||
setShowActionStatusModal={setShowActionStatusModal}
|
|
||||||
setPreviewDocument={setPreviewDocument}
|
|
||||||
setDocumentError={setDocumentError}
|
|
||||||
setSkipApproverData={setSkipApproverData}
|
|
||||||
setActionStatus={setActionStatus}
|
|
||||||
handleApproveConfirm={handleApproveConfirm}
|
|
||||||
handleRejectConfirm={handleRejectConfirm}
|
|
||||||
handleAddApprover={handleAddApprover}
|
|
||||||
handleAddSpectator={handleAddSpectator}
|
|
||||||
handleSkipApprover={handleSkipApprover}
|
|
||||||
downloadDocument={downloadDocument}
|
|
||||||
documentPolicy={documentPolicy}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dealer Claim RequestDetail Component (Exported)
|
|
||||||
*/
|
|
||||||
export function DealerClaimRequestDetail(props: RequestDetailProps) {
|
|
||||||
return (
|
|
||||||
<RequestDetailErrorBoundary>
|
|
||||||
<DealerClaimRequestDetailInner {...props} />
|
|
||||||
</RequestDetailErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
93
src/flows.ts
93
src/flows.ts
@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* Request Flow Registry
|
|
||||||
*
|
|
||||||
* Central registry for all request flow types.
|
|
||||||
* This provides a single import point for flow-specific components.
|
|
||||||
*
|
|
||||||
* LOCATION: src/flows.ts
|
|
||||||
*
|
|
||||||
* This file imports from flow folders at src/ level:
|
|
||||||
* - src/custom/
|
|
||||||
* - src/dealer-claim/
|
|
||||||
* - src/shared/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RequestFlowType } from '@/utils/requestTypeUtils';
|
|
||||||
|
|
||||||
// Import flow modules from src/ level
|
|
||||||
import * as CustomFlow from './custom';
|
|
||||||
import * as DealerClaimFlow from './dealer-claim';
|
|
||||||
import * as SharedComponents from './shared/components';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flow registry mapping
|
|
||||||
* Maps RequestFlowType to their respective flow modules
|
|
||||||
*/
|
|
||||||
export const FlowRegistry = {
|
|
||||||
CUSTOM: CustomFlow,
|
|
||||||
DEALER_CLAIM: DealerClaimFlow,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get flow module for a given flow type
|
|
||||||
*/
|
|
||||||
export function getFlowModule(flowType: RequestFlowType) {
|
|
||||||
return FlowRegistry[flowType];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get overview tab component for a flow type
|
|
||||||
*/
|
|
||||||
export function getOverviewTab(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.DealerClaimOverviewTab;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomOverviewTab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get workflow tab component for a flow type
|
|
||||||
*/
|
|
||||||
export function getWorkflowTab(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.DealerClaimWorkflowTab;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomWorkflowTab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get create request component for a flow type
|
|
||||||
*/
|
|
||||||
export function getCreateRequestComponent(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.ClaimManagementWizard;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomCreateRequest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get RequestDetail screen component for a flow type
|
|
||||||
* Each flow has its own complete RequestDetail screen
|
|
||||||
*/
|
|
||||||
export function getRequestDetailScreen(flowType: RequestFlowType) {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return DealerClaimFlow.DealerClaimRequestDetail;
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return CustomFlow.CustomRequestDetail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export flow modules for direct access
|
|
||||||
export { CustomFlow, DealerClaimFlow, SharedComponents };
|
|
||||||
export type { RequestFlowType } from '@/utils/requestTypeUtils';
|
|
||||||
@ -68,16 +68,7 @@ export function ApproverPerformanceRequestList({
|
|||||||
<Card
|
<Card
|
||||||
key={request.requestId}
|
key={request.requestId}
|
||||||
className="hover:shadow-md transition-shadow cursor-pointer"
|
className="hover:shadow-md transition-shadow cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => navigate(`/request/${request.requestId}`)}
|
||||||
const { navigateToRequest } = require('@/utils/requestNavigation');
|
|
||||||
navigateToRequest({
|
|
||||||
requestId: request.requestId,
|
|
||||||
requestTitle: request.title,
|
|
||||||
status: request.status,
|
|
||||||
request: request,
|
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
data-testid={`request-card-${request.requestId}`}
|
data-testid={`request-card-${request.requestId}`}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
@ -166,14 +157,7 @@ export function ApproverPerformanceRequestList({
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const { navigateToRequest } = require('@/utils/requestNavigation');
|
navigate(`/request/${request.requestId}`);
|
||||||
navigateToRequest({
|
|
||||||
requestId: request.requestId,
|
|
||||||
requestTitle: request.title,
|
|
||||||
status: request.status,
|
|
||||||
request: request,
|
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
data-testid="view-request-button"
|
data-testid="view-request-button"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -69,11 +69,7 @@ export function DetailedReports({ onBack }: DetailedReportsProps) {
|
|||||||
}, [onBack, navigate]);
|
}, [onBack, navigate]);
|
||||||
|
|
||||||
const handleViewRequest = useCallback((requestId: string) => {
|
const handleViewRequest = useCallback((requestId: string) => {
|
||||||
const { navigateToRequest } = require('@/utils/requestNavigation');
|
navigate(`/request/${requestId}`);
|
||||||
navigateToRequest({
|
|
||||||
requestId,
|
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// Export handlers
|
// Export handlers
|
||||||
|
|||||||
@ -1,26 +1,65 @@
|
|||||||
/**
|
/**
|
||||||
* RequestDetail Router Component
|
* RequestDetail Component
|
||||||
*
|
*
|
||||||
* Purpose: Routes to the appropriate flow-specific RequestDetail screen
|
* Purpose: Display and manage detailed view of a workflow request
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Architecture:
|
||||||
* - This is a router that determines the flow type and renders the appropriate screen
|
* - Uses custom hooks for complex logic (data fetching, socket, document upload, etc.)
|
||||||
* - Each flow has its own complete RequestDetail screen in its folder
|
* - Delegates UI rendering to specialized tab components
|
||||||
* - Deleting a flow folder removes all related code (truly modular)
|
* - Error boundary for graceful error handling
|
||||||
*
|
* - Real-time WebSocket integration
|
||||||
* IMPORTANT: This file only routes. All actual implementation is in flow-specific folders.
|
|
||||||
* Deleting src/custom/ or src/dealer-claim/ removes ALL related code.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, ErrorInfo, ReactNode } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useRequestDetails } from '@/hooks/useRequestDetails';
|
import {
|
||||||
|
ClipboardList,
|
||||||
|
TrendingUp,
|
||||||
|
FileText,
|
||||||
|
Activity,
|
||||||
|
MessageSquare,
|
||||||
|
AlertTriangle,
|
||||||
|
FileCheck,
|
||||||
|
ShieldX,
|
||||||
|
RefreshCw,
|
||||||
|
ArrowLeft,
|
||||||
|
DollarSign,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
// Context and hooks
|
||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { getRequestFlowType } from '@/utils/requestTypeUtils';
|
import { useRequestDetails } from '@/hooks/useRequestDetails';
|
||||||
import { getRequestDetailScreen } from '@/flows';
|
import { useRequestSocket } from '@/hooks/useRequestSocket';
|
||||||
|
import { useDocumentUpload } from '@/hooks/useDocumentUpload';
|
||||||
|
import { useConclusionRemark } from '@/hooks/useConclusionRemark';
|
||||||
|
import { useModalManager } from '@/hooks/useModalManager';
|
||||||
|
import { downloadDocument } from '@/services/workflowApi';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { RequestDetailHeader } from './components/RequestDetailHeader';
|
||||||
|
import { ShareSummaryModal } from '@/components/modals/ShareSummaryModal';
|
||||||
|
import { getSummaryDetails, getSummaryByRequestId, type SummaryDetails } from '@/services/summaryApi';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { OverviewTab } from './components/tabs/OverviewTab';
|
||||||
|
import { ClaimManagementOverviewTab } from './components/tabs/ClaimManagementOverviewTab';
|
||||||
|
import { WorkflowTab } from './components/tabs/WorkflowTab';
|
||||||
|
import { DealerClaimWorkflowTab } from './components/tabs/DealerClaimWorkflowTab';
|
||||||
|
import { DocumentsTab } from './components/tabs/DocumentsTab';
|
||||||
|
import { ActivityTab } from './components/tabs/ActivityTab';
|
||||||
|
import { WorkNotesTab } from './components/tabs/WorkNotesTab';
|
||||||
|
import { SummaryTab } from './components/tabs/SummaryTab';
|
||||||
|
import { IOTab } from './components/tabs/IOTab';
|
||||||
|
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
||||||
|
import { QuickActionsSidebar } from './components/QuickActionsSidebar';
|
||||||
|
import { RequestDetailModals } from './components/RequestDetailModals';
|
||||||
import { RequestDetailProps } from './types/requestDetail.types';
|
import { RequestDetailProps } from './types/requestDetail.types';
|
||||||
|
import { PauseModal } from '@/components/workflow/PauseModal';
|
||||||
|
import { ResumeModal } from '@/components/workflow/ResumeModal';
|
||||||
|
import { RetriggerPauseModal } from '@/components/workflow/RetriggerPauseModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error Boundary Component
|
* Error Boundary Component
|
||||||
@ -36,7 +75,7 @@ class RequestDetailErrorBoundary extends Component<{ children: ReactNode }, { ha
|
|||||||
}
|
}
|
||||||
|
|
||||||
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
console.error('RequestDetail Router Error:', error, errorInfo);
|
console.error('RequestDetail Error:', error, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
@ -62,24 +101,277 @@ class RequestDetailErrorBoundary extends Component<{ children: ReactNode }, { ha
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RequestDetailRouter Component
|
* RequestDetailInner Component
|
||||||
*
|
|
||||||
* Routes to the appropriate flow-specific RequestDetail screen based on request type.
|
|
||||||
* This ensures complete modularity - deleting a flow folder removes all related code.
|
|
||||||
*/
|
*/
|
||||||
function RequestDetailRouter({ requestId: propRequestId, onBack, dynamicRequests = [] }: RequestDetailProps) {
|
function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests = [] }: RequestDetailProps) {
|
||||||
const params = useParams<{ requestId: string }>();
|
const params = useParams<{ requestId: string }>();
|
||||||
const requestIdentifier = params.requestId || propRequestId || '';
|
const requestIdentifier = params.requestId || propRequestId || '';
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const initialTab = urlParams.get('tab') || 'overview';
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
|
const [showShareSummaryModal, setShowShareSummaryModal] = useState(false);
|
||||||
|
const [summaryId, setSummaryId] = useState<string | null>(null);
|
||||||
|
const [summaryDetails, setSummaryDetails] = useState<SummaryDetails | null>(null);
|
||||||
|
const [loadingSummary, setLoadingSummary] = useState(false);
|
||||||
|
const [sharedRecipientsRefreshTrigger, setSharedRecipientsRefreshTrigger] = useState(0);
|
||||||
|
const [showPauseModal, setShowPauseModal] = useState(false);
|
||||||
|
const [showResumeModal, setShowResumeModal] = useState(false);
|
||||||
|
const [showRetriggerModal, setShowRetriggerModal] = useState(false);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Fetch request details to determine flow type
|
// Custom hooks
|
||||||
const {
|
const {
|
||||||
|
request,
|
||||||
apiRequest,
|
apiRequest,
|
||||||
loading: requestLoading,
|
loading: requestLoading,
|
||||||
|
refreshing,
|
||||||
|
refreshDetails,
|
||||||
|
currentApprovalLevel,
|
||||||
|
isSpectator,
|
||||||
|
isInitiator,
|
||||||
|
existingParticipants,
|
||||||
|
accessDenied,
|
||||||
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
} = useRequestDetails(requestIdentifier, dynamicRequests, user);
|
||||||
|
|
||||||
// Loading state while determining flow type
|
// Determine if user is initiator (from overview tab initiator info)
|
||||||
if (requestLoading && !apiRequest) {
|
const currentUserId = (user as any)?.userId || '';
|
||||||
|
const currentUserEmail = (user as any)?.email?.toLowerCase() || '';
|
||||||
|
const initiatorUserId = apiRequest?.initiator?.userId;
|
||||||
|
const initiatorEmail = apiRequest?.initiator?.email?.toLowerCase();
|
||||||
|
const isUserInitiator = apiRequest?.initiator && (
|
||||||
|
(initiatorUserId && initiatorUserId === currentUserId) ||
|
||||||
|
(initiatorEmail && initiatorEmail === currentUserEmail)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine if user is department lead (whoever is in step 3 / approval level 3)
|
||||||
|
const approvalLevels = apiRequest?.approvalLevels || [];
|
||||||
|
const step3Level = approvalLevels.find((level: any) =>
|
||||||
|
(level.levelNumber || level.level_number) === 3
|
||||||
|
);
|
||||||
|
const deptLeadUserId = step3Level?.approverId || step3Level?.approver?.userId;
|
||||||
|
const deptLeadEmail = (step3Level?.approverEmail || step3Level?.approver?.email || step3Level?.approverEmail || '').toLowerCase().trim();
|
||||||
|
|
||||||
|
// Check if user is department lead by userId or email (case-insensitive)
|
||||||
|
const isDeptLead = (deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||||
|
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail);
|
||||||
|
|
||||||
|
// Get step 3 status (case-insensitive check)
|
||||||
|
const step3Status = step3Level?.status ? String(step3Level.status).toUpperCase() : '';
|
||||||
|
const isStep3PendingOrInProgress = step3Status === 'PENDING' ||
|
||||||
|
step3Status === 'IN_PROGRESS';
|
||||||
|
|
||||||
|
// Check if user is current approver for step 3 (can access IO tab when step is pending/in-progress)
|
||||||
|
// Also check if currentLevel is 3 (workflow is at step 3)
|
||||||
|
const currentLevel = apiRequest?.currentLevel || apiRequest?.current_level || 0;
|
||||||
|
const isStep3CurrentLevel = currentLevel === 3;
|
||||||
|
|
||||||
|
const isStep3CurrentApprover = step3Level && isStep3PendingOrInProgress && isStep3CurrentLevel && (
|
||||||
|
(deptLeadUserId && deptLeadUserId === currentUserId) ||
|
||||||
|
(deptLeadEmail && currentUserEmail && deptLeadEmail === currentUserEmail)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if IO tab should be visible (for initiator and department lead in claim management requests)
|
||||||
|
// Department lead can access IO tab when they are the current approver for step 3 (to fetch and block IO)
|
||||||
|
const showIOTab = isClaimManagementRequest(apiRequest) &&
|
||||||
|
(isUserInitiator || isDeptLead || isStep3CurrentApprover);
|
||||||
|
|
||||||
|
// Debug logging for troubleshooting
|
||||||
|
console.debug('[RequestDetail] IO Tab visibility:', {
|
||||||
|
isClaimManagement: isClaimManagementRequest(apiRequest),
|
||||||
|
isUserInitiator,
|
||||||
|
isDeptLead,
|
||||||
|
isStep3CurrentApprover,
|
||||||
|
currentUserId,
|
||||||
|
currentUserEmail,
|
||||||
|
initiatorUserId,
|
||||||
|
initiatorEmail,
|
||||||
|
currentLevel,
|
||||||
|
isStep3CurrentLevel,
|
||||||
|
step3Level: step3Level ? {
|
||||||
|
levelNumber: step3Level.levelNumber || step3Level.level_number,
|
||||||
|
approverId: step3Level.approverId || step3Level.approver?.userId,
|
||||||
|
approverEmail: step3Level.approverEmail || step3Level.approver?.email,
|
||||||
|
status: step3Level.status,
|
||||||
|
statusUpper: step3Status,
|
||||||
|
isPendingOrInProgress: isStep3PendingOrInProgress
|
||||||
|
} : null,
|
||||||
|
deptLeadUserId,
|
||||||
|
deptLeadEmail,
|
||||||
|
emailMatch: deptLeadEmail && currentUserEmail ? deptLeadEmail === currentUserEmail : false,
|
||||||
|
showIOTab,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mergedMessages,
|
||||||
|
unreadWorkNotes,
|
||||||
|
workNoteAttachments,
|
||||||
|
setWorkNoteAttachments,
|
||||||
|
} = useRequestSocket(requestIdentifier, apiRequest, activeTab, user);
|
||||||
|
|
||||||
|
const {
|
||||||
|
uploadingDocument,
|
||||||
|
triggerFileInput,
|
||||||
|
previewDocument,
|
||||||
|
setPreviewDocument,
|
||||||
|
documentPolicy,
|
||||||
|
documentError,
|
||||||
|
setDocumentError,
|
||||||
|
} = useDocumentUpload(apiRequest, refreshDetails);
|
||||||
|
|
||||||
|
const {
|
||||||
|
showApproveModal,
|
||||||
|
setShowApproveModal,
|
||||||
|
showRejectModal,
|
||||||
|
setShowRejectModal,
|
||||||
|
showAddApproverModal,
|
||||||
|
setShowAddApproverModal,
|
||||||
|
showAddSpectatorModal,
|
||||||
|
setShowAddSpectatorModal,
|
||||||
|
showSkipApproverModal,
|
||||||
|
setShowSkipApproverModal,
|
||||||
|
showActionStatusModal,
|
||||||
|
setShowActionStatusModal,
|
||||||
|
skipApproverData,
|
||||||
|
setSkipApproverData,
|
||||||
|
actionStatus,
|
||||||
|
setActionStatus,
|
||||||
|
handleApproveConfirm,
|
||||||
|
handleRejectConfirm,
|
||||||
|
handleAddApprover,
|
||||||
|
handleSkipApprover,
|
||||||
|
handleAddSpectator,
|
||||||
|
} = useModalManager(requestIdentifier, currentApprovalLevel, refreshDetails);
|
||||||
|
|
||||||
|
const {
|
||||||
|
conclusionRemark,
|
||||||
|
setConclusionRemark,
|
||||||
|
conclusionLoading,
|
||||||
|
conclusionSubmitting,
|
||||||
|
aiGenerated,
|
||||||
|
handleGenerateConclusion,
|
||||||
|
handleFinalizeConclusion,
|
||||||
|
} = useConclusionRemark(request, requestIdentifier, isInitiator, refreshDetails, onBack, setActionStatus, setShowActionStatusModal);
|
||||||
|
|
||||||
|
// Auto-switch tab when URL query parameter changes
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const tabParam = urlParams.get('tab');
|
||||||
|
if (tabParam) {
|
||||||
|
setActiveTab(tabParam);
|
||||||
|
}
|
||||||
|
}, [requestIdentifier]);
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
refreshDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pause handlers
|
||||||
|
const handlePause = () => {
|
||||||
|
setShowPauseModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResume = () => {
|
||||||
|
setShowResumeModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResumeSuccess = async () => {
|
||||||
|
// Wait for refresh to complete to show updated status
|
||||||
|
await refreshDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRetrigger = () => {
|
||||||
|
setShowRetriggerModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePauseSuccess = async () => {
|
||||||
|
// Wait for refresh to complete to show updated pause status
|
||||||
|
await refreshDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRetriggerSuccess = async () => {
|
||||||
|
// Wait for refresh to complete
|
||||||
|
await refreshDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShareSummary = async () => {
|
||||||
|
if (!apiRequest?.requestId) {
|
||||||
|
toast.error('Request ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summaryId) {
|
||||||
|
toast.error('Summary not available. Please ensure the request is closed and the summary has been generated.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open share modal with the existing summary ID
|
||||||
|
// Summary should already exist from closure (auto-created by backend)
|
||||||
|
setShowShareSummaryModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator;
|
||||||
|
|
||||||
|
// Check if request is closed (or needs closure for approved/rejected)
|
||||||
|
const isClosed = request?.status === 'closed' || (request?.status === 'approved' && !isInitiator) || (request?.status === 'rejected' && !isInitiator);
|
||||||
|
|
||||||
|
// Fetch summary details if request is closed
|
||||||
|
// Summary is automatically created by backend when request is closed (on final approval)
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSummaryDetails = async () => {
|
||||||
|
if (!isClosed || !apiRequest?.requestId) {
|
||||||
|
setSummaryDetails(null);
|
||||||
|
setSummaryId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingSummary(true);
|
||||||
|
// Just fetch the summary by requestId - don't try to create it
|
||||||
|
// Summary is auto-created by backend on final approval/rejection
|
||||||
|
const summary = await getSummaryByRequestId(apiRequest.requestId);
|
||||||
|
|
||||||
|
if (summary?.summaryId) {
|
||||||
|
setSummaryId(summary.summaryId);
|
||||||
|
// Fetch full summary details
|
||||||
|
try {
|
||||||
|
const details = await getSummaryDetails(summary.summaryId);
|
||||||
|
setSummaryDetails(details);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to fetch summary details:', error);
|
||||||
|
setSummaryDetails(null);
|
||||||
|
setSummaryId(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Summary doesn't exist yet - this is normal if request just closed
|
||||||
|
setSummaryDetails(null);
|
||||||
|
setSummaryId(null);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Summary not found - this is OK, summary may not exist yet
|
||||||
|
setSummaryDetails(null);
|
||||||
|
setSummaryId(null);
|
||||||
|
} finally {
|
||||||
|
setLoadingSummary(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchSummaryDetails();
|
||||||
|
}, [isClosed, apiRequest?.requestId]);
|
||||||
|
|
||||||
|
// Get current levels for WorkNotesTab
|
||||||
|
const 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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (requestLoading && !request && !apiRequest) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen bg-gray-50" data-testid="loading-state">
|
<div className="flex items-center justify-center h-screen bg-gray-50" data-testid="loading-state">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@ -90,36 +382,406 @@ function RequestDetailRouter({ requestId: propRequestId, onBack, dynamicRequests
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine flow type and get the appropriate RequestDetail screen
|
// Access Denied state
|
||||||
const flowType = getRequestFlowType(apiRequest);
|
if (accessDenied?.denied) {
|
||||||
const RequestDetailScreen = getRequestDetailScreen(flowType);
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="access-denied-state">
|
||||||
|
<div className="max-w-lg w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
||||||
|
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<ShieldX className="w-10 h-10 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">Access Denied</h2>
|
||||||
|
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||||
|
{accessDenied.message}
|
||||||
|
</p>
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6 text-left">
|
||||||
|
<p className="text-sm text-amber-800">
|
||||||
|
<strong>Who can access this request?</strong>
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-amber-700 mt-2 space-y-1">
|
||||||
|
<li>• The person who created this request (Initiator)</li>
|
||||||
|
<li>• Designated approvers at any level</li>
|
||||||
|
<li>• Added spectators or participants</li>
|
||||||
|
<li>• Organization administrators</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onBack || (() => window.history.back())}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.href = '/dashboard'}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Go to Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not Found state
|
||||||
|
if (!request) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-6" data-testid="not-found-state">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-2xl shadow-xl p-8 text-center">
|
||||||
|
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<FileText className="w-10 h-10 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">Request Not Found</h2>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
The request you're looking for doesn't exist or may have been deleted.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3 justify-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onBack || (() => window.history.back())}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.href = '/dashboard'}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Go to Dashboard
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Render the flow-specific RequestDetail screen
|
|
||||||
// Each flow has its own complete implementation in its folder
|
|
||||||
return (
|
return (
|
||||||
<RequestDetailScreen
|
<>
|
||||||
requestId={propRequestId}
|
<div className="min-h-screen bg-gray-50" data-testid="request-detail-page">
|
||||||
onBack={onBack}
|
<div className="max-w-7xl mx-auto">
|
||||||
dynamicRequests={dynamicRequests}
|
{/* Header Section */}
|
||||||
/>
|
<RequestDetailHeader
|
||||||
|
request={request}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onBack={onBack || (() => window.history.back())}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
onShareSummary={handleShareSummary}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="request-detail-tabs">
|
||||||
|
<div className="mb-4 sm:mb-6">
|
||||||
|
<TabsList className="grid grid-cols-3 sm:grid-cols-6 lg:flex lg:flex-row h-auto bg-gray-100 p-1.5 sm:p-1 rounded-lg gap-1.5 sm:gap-1">
|
||||||
|
<TabsTrigger
|
||||||
|
value="overview"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-overview"
|
||||||
|
>
|
||||||
|
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Overview</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
{isClosed && summaryDetails && (
|
||||||
|
<TabsTrigger
|
||||||
|
value="summary"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-summary"
|
||||||
|
>
|
||||||
|
<FileCheck className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Summary</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger
|
||||||
|
value="workflow"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-workflow"
|
||||||
|
>
|
||||||
|
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Workflow</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
{showIOTab && (
|
||||||
|
<TabsTrigger
|
||||||
|
value="io"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-io"
|
||||||
|
>
|
||||||
|
<DollarSign className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">IO</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger
|
||||||
|
value="documents"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
|
||||||
|
data-testid="tab-documents"
|
||||||
|
>
|
||||||
|
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Docs</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="activity"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 col-span-1 sm:col-span-1"
|
||||||
|
data-testid="tab-activity"
|
||||||
|
>
|
||||||
|
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Activity</span>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="worknotes"
|
||||||
|
className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 relative col-span-2 sm:col-span-1"
|
||||||
|
data-testid="tab-worknotes"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
|
<span className="truncate">Work Notes</span>
|
||||||
|
{unreadWorkNotes > 0 && (
|
||||||
|
<Badge
|
||||||
|
className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0"
|
||||||
|
data-testid="worknotes-unread-badge"
|
||||||
|
>
|
||||||
|
{unreadWorkNotes > 9 ? '9+' : unreadWorkNotes}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Layout */}
|
||||||
|
<div className={activeTab === 'worknotes' ? '' : 'grid grid-cols-1 lg:grid-cols-3 gap-6'}>
|
||||||
|
{/* Left Column: Tab content */}
|
||||||
|
<div className={activeTab === 'worknotes' ? '' : 'lg:col-span-2'}>
|
||||||
|
<TabsContent value="overview" className="mt-0" data-testid="overview-tab-content">
|
||||||
|
{isClaimManagementRequest(apiRequest) ? (
|
||||||
|
<ClaimManagementOverviewTab
|
||||||
|
request={request}
|
||||||
|
apiRequest={apiRequest}
|
||||||
|
currentUserId={(user as any)?.userId || ''}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<OverviewTab
|
||||||
|
request={request}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
needsClosure={needsClosure}
|
||||||
|
conclusionRemark={conclusionRemark}
|
||||||
|
setConclusionRemark={setConclusionRemark}
|
||||||
|
conclusionLoading={conclusionLoading}
|
||||||
|
conclusionSubmitting={conclusionSubmitting}
|
||||||
|
aiGenerated={aiGenerated}
|
||||||
|
handleGenerateConclusion={handleGenerateConclusion}
|
||||||
|
handleFinalizeConclusion={handleFinalizeConclusion}
|
||||||
|
onPause={handlePause}
|
||||||
|
onResume={handleResume}
|
||||||
|
onRetrigger={handleRetrigger}
|
||||||
|
currentUserIsApprover={!!currentApprovalLevel}
|
||||||
|
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
||||||
|
currentUserId={(user as any)?.userId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{isClosed && (
|
||||||
|
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
|
||||||
|
<SummaryTab
|
||||||
|
summary={summaryDetails}
|
||||||
|
loading={loadingSummary}
|
||||||
|
onShare={handleShareSummary}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TabsContent value="workflow" className="mt-0">
|
||||||
|
{isClaimManagementRequest(apiRequest) ? (
|
||||||
|
<DealerClaimWorkflowTab
|
||||||
|
request={request}
|
||||||
|
user={user}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
onSkipApprover={(data) => {
|
||||||
|
if (!data.levelId) {
|
||||||
|
alert('Level ID not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSkipApproverData(data);
|
||||||
|
setShowSkipApproverModal(true);
|
||||||
|
}}
|
||||||
|
onRefresh={refreshDetails}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<WorkflowTab
|
||||||
|
request={request}
|
||||||
|
user={user}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
onSkipApprover={(data) => {
|
||||||
|
if (!data.levelId) {
|
||||||
|
alert('Level ID not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSkipApproverData(data);
|
||||||
|
setShowSkipApproverModal(true);
|
||||||
|
}}
|
||||||
|
onRefresh={refreshDetails}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{showIOTab && (
|
||||||
|
<TabsContent value="io" className="mt-0">
|
||||||
|
<IOTab
|
||||||
|
request={request}
|
||||||
|
apiRequest={apiRequest}
|
||||||
|
onRefresh={refreshDetails}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TabsContent value="documents" className="mt-0">
|
||||||
|
<DocumentsTab
|
||||||
|
request={request}
|
||||||
|
workNoteAttachments={workNoteAttachments}
|
||||||
|
uploadingDocument={uploadingDocument}
|
||||||
|
documentPolicy={documentPolicy}
|
||||||
|
triggerFileInput={triggerFileInput}
|
||||||
|
setPreviewDocument={setPreviewDocument}
|
||||||
|
downloadDocument={downloadDocument}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="activity" className="mt-0">
|
||||||
|
<ActivityTab request={request} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="worknotes" className="mt-0" forceMount={true} hidden={activeTab !== 'worknotes'}>
|
||||||
|
<WorkNotesTab
|
||||||
|
requestId={requestIdentifier}
|
||||||
|
requestTitle={request.title}
|
||||||
|
mergedMessages={mergedMessages}
|
||||||
|
setWorkNoteAttachments={setWorkNoteAttachments}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
isSpectator={isSpectator}
|
||||||
|
currentLevels={currentLevels}
|
||||||
|
onAddApprover={handleAddApprover}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Quick Actions Sidebar */}
|
||||||
|
{activeTab !== 'worknotes' && (
|
||||||
|
<QuickActionsSidebar
|
||||||
|
request={request}
|
||||||
|
isInitiator={isInitiator}
|
||||||
|
isSpectator={isSpectator}
|
||||||
|
currentApprovalLevel={currentApprovalLevel}
|
||||||
|
onAddApprover={() => setShowAddApproverModal(true)}
|
||||||
|
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
||||||
|
onApprove={() => setShowApproveModal(true)}
|
||||||
|
onReject={() => setShowRejectModal(true)}
|
||||||
|
onPause={handlePause}
|
||||||
|
onResume={handleResume}
|
||||||
|
onRetrigger={handleRetrigger}
|
||||||
|
summaryId={summaryId}
|
||||||
|
refreshTrigger={sharedRecipientsRefreshTrigger}
|
||||||
|
pausedByUserId={request?.pauseInfo?.pausedBy?.userId}
|
||||||
|
currentUserId={(user as any)?.userId}
|
||||||
|
apiRequest={apiRequest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Share Summary Modal */}
|
||||||
|
{showShareSummaryModal && summaryId && (
|
||||||
|
<ShareSummaryModal
|
||||||
|
isOpen={showShareSummaryModal}
|
||||||
|
onClose={() => setShowShareSummaryModal(false)}
|
||||||
|
summaryId={summaryId}
|
||||||
|
requestTitle={request?.title || 'N/A'}
|
||||||
|
onSuccess={() => {
|
||||||
|
refreshDetails();
|
||||||
|
// Trigger refresh of shared recipients list
|
||||||
|
setSharedRecipientsRefreshTrigger(prev => prev + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pause Modals */}
|
||||||
|
{showPauseModal && apiRequest?.requestId && (
|
||||||
|
<PauseModal
|
||||||
|
isOpen={showPauseModal}
|
||||||
|
onClose={() => setShowPauseModal(false)}
|
||||||
|
requestId={apiRequest.requestId}
|
||||||
|
levelId={currentApprovalLevel?.levelId || null}
|
||||||
|
onSuccess={handlePauseSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showResumeModal && apiRequest?.requestId && (
|
||||||
|
<ResumeModal
|
||||||
|
isOpen={showResumeModal}
|
||||||
|
onClose={() => setShowResumeModal(false)}
|
||||||
|
requestId={apiRequest.requestId}
|
||||||
|
onSuccess={handleResumeSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showRetriggerModal && apiRequest?.requestId && (
|
||||||
|
<RetriggerPauseModal
|
||||||
|
isOpen={showRetriggerModal}
|
||||||
|
onClose={() => setShowRetriggerModal(false)}
|
||||||
|
requestId={apiRequest.requestId}
|
||||||
|
approverName={request?.pauseInfo?.pausedBy?.name}
|
||||||
|
onSuccess={handleRetriggerSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
<RequestDetailModals
|
||||||
|
showApproveModal={showApproveModal}
|
||||||
|
showRejectModal={showRejectModal}
|
||||||
|
showAddApproverModal={showAddApproverModal}
|
||||||
|
showAddSpectatorModal={showAddSpectatorModal}
|
||||||
|
showSkipApproverModal={showSkipApproverModal}
|
||||||
|
showActionStatusModal={showActionStatusModal}
|
||||||
|
previewDocument={previewDocument}
|
||||||
|
documentError={documentError}
|
||||||
|
request={request}
|
||||||
|
skipApproverData={skipApproverData}
|
||||||
|
actionStatus={actionStatus}
|
||||||
|
existingParticipants={existingParticipants}
|
||||||
|
currentLevels={currentLevels}
|
||||||
|
setShowApproveModal={setShowApproveModal}
|
||||||
|
setShowRejectModal={setShowRejectModal}
|
||||||
|
setShowAddApproverModal={setShowAddApproverModal}
|
||||||
|
setShowAddSpectatorModal={setShowAddSpectatorModal}
|
||||||
|
setShowSkipApproverModal={setShowSkipApproverModal}
|
||||||
|
setShowActionStatusModal={setShowActionStatusModal}
|
||||||
|
setPreviewDocument={setPreviewDocument}
|
||||||
|
setDocumentError={setDocumentError}
|
||||||
|
setSkipApproverData={setSkipApproverData}
|
||||||
|
setActionStatus={setActionStatus}
|
||||||
|
handleApproveConfirm={handleApproveConfirm}
|
||||||
|
handleRejectConfirm={handleRejectConfirm}
|
||||||
|
handleAddApprover={handleAddApprover}
|
||||||
|
handleAddSpectator={handleAddSpectator}
|
||||||
|
handleSkipApprover={handleSkipApprover}
|
||||||
|
downloadDocument={downloadDocument}
|
||||||
|
documentPolicy={documentPolicy}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RequestDetail Component (Exported)
|
* RequestDetail Component (Exported)
|
||||||
*
|
|
||||||
* This is now a router that delegates to flow-specific RequestDetail screens.
|
|
||||||
* Each flow has its own complete RequestDetail implementation in its folder.
|
|
||||||
*
|
|
||||||
* To remove a flow type completely:
|
|
||||||
* 1. Delete the flow folder (e.g., src/dealer-claim/)
|
|
||||||
* 2. Remove it from src/flows.ts FlowRegistry
|
|
||||||
* 3. That's it! All related code is gone.
|
|
||||||
*/
|
*/
|
||||||
export function RequestDetail(props: RequestDetailProps) {
|
export function RequestDetail(props: RequestDetailProps) {
|
||||||
return (
|
return (
|
||||||
<RequestDetailErrorBoundary>
|
<RequestDetailErrorBoundary>
|
||||||
<RequestDetailRouter {...props} />
|
<RequestDetailInner {...props} />
|
||||||
</RequestDetailErrorBoundary>
|
</RequestDetailErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Components for Request Flows
|
|
||||||
*
|
|
||||||
* Common components that are reused across different request flow types.
|
|
||||||
* These components are flow-agnostic and provide consistent UI/UX.
|
|
||||||
*
|
|
||||||
* LOCATION: src/shared/components/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Import individual components
|
|
||||||
import { DocumentsTab } from './request-detail/DocumentsTab';
|
|
||||||
import { ActivityTab } from './request-detail/ActivityTab';
|
|
||||||
import { WorkNotesTab } from './request-detail/WorkNotesTab';
|
|
||||||
import { SummaryTab } from './request-detail/SummaryTab';
|
|
||||||
import { RequestDetailHeader } from './request-detail/RequestDetailHeader';
|
|
||||||
import { QuickActionsSidebar } from './request-detail/QuickActionsSidebar';
|
|
||||||
import { RequestDetailModals } from './request-detail/RequestDetailModals';
|
|
||||||
|
|
||||||
// Export individual components
|
|
||||||
export { DocumentsTab } from './request-detail/DocumentsTab';
|
|
||||||
export { ActivityTab } from './request-detail/ActivityTab';
|
|
||||||
export { WorkNotesTab } from './request-detail/WorkNotesTab';
|
|
||||||
export { SummaryTab } from './request-detail/SummaryTab';
|
|
||||||
export { RequestDetailHeader } from './request-detail/RequestDetailHeader';
|
|
||||||
export { QuickActionsSidebar } from './request-detail/QuickActionsSidebar';
|
|
||||||
export { RequestDetailModals } from './request-detail/RequestDetailModals';
|
|
||||||
|
|
||||||
// Export as a named object for convenience
|
|
||||||
export const SharedComponents = {
|
|
||||||
DocumentsTab,
|
|
||||||
ActivityTab,
|
|
||||||
WorkNotesTab,
|
|
||||||
SummaryTab,
|
|
||||||
RequestDetailHeader,
|
|
||||||
QuickActionsSidebar,
|
|
||||||
RequestDetailModals,
|
|
||||||
};
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Activity Tab
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { ActivityTab } from '@/pages/RequestDetail/components/tabs/ActivityTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Documents Tab
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { DocumentsTab } from '@/pages/RequestDetail/components/tabs/DocumentsTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Quick Actions Sidebar
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { QuickActionsSidebar } from '@/pages/RequestDetail/components/QuickActionsSidebar';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Request Detail Header
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { RequestDetailHeader } from '@/pages/RequestDetail/components/RequestDetailHeader';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Request Detail Modals
|
|
||||||
*
|
|
||||||
* These modals are shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { RequestDetailModals } from '@/pages/RequestDetail/components/RequestDetailModals';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Summary Tab
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { SummaryTab } from '@/pages/RequestDetail/components/tabs/SummaryTab';
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared Work Notes Tab
|
|
||||||
*
|
|
||||||
* This component is shared across all request flow types.
|
|
||||||
* Located in: src/shared/components/request-detail/
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export the original component
|
|
||||||
export { WorkNotesTab } from '@/pages/RequestDetail/components/tabs/WorkNotesTab';
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Global Request Navigation Utility
|
|
||||||
*
|
|
||||||
* Centralized navigation logic for request-related routes.
|
|
||||||
* This utility decides where to navigate when clicking on request cards
|
|
||||||
* from anywhere in the application.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Single point of navigation logic
|
|
||||||
* - Handles draft vs active requests
|
|
||||||
* - Supports different flow types (CUSTOM, DEALER_CLAIM)
|
|
||||||
* - Type-safe navigation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NavigateFunction } from 'react-router-dom';
|
|
||||||
import { getRequestDetailRoute, getRequestFlowType, RequestFlowType } from './requestTypeUtils';
|
|
||||||
|
|
||||||
export interface RequestNavigationOptions {
|
|
||||||
requestId: string;
|
|
||||||
requestTitle?: string;
|
|
||||||
status?: string;
|
|
||||||
request?: any; // Full request object if available
|
|
||||||
navigate: NavigateFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the appropriate request detail page based on request type
|
|
||||||
*
|
|
||||||
* This is the single point of navigation for all request cards.
|
|
||||||
* It handles:
|
|
||||||
* - Draft requests (navigate to edit)
|
|
||||||
* - Different flow types (CUSTOM, DEALER_CLAIM)
|
|
||||||
* - Status-based routing
|
|
||||||
*/
|
|
||||||
export function navigateToRequest(options: RequestNavigationOptions): void {
|
|
||||||
const { requestId, requestTitle, status, request, navigate } = options;
|
|
||||||
|
|
||||||
// Check if request is a draft - if so, route to edit form instead of detail view
|
|
||||||
const isDraft = status?.toLowerCase() === 'draft' || status === 'DRAFT';
|
|
||||||
if (isDraft) {
|
|
||||||
navigate(`/edit-request/${requestId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the appropriate route based on request type
|
|
||||||
const route = getRequestDetailRoute(requestId, request);
|
|
||||||
navigate(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to create a new request based on flow type
|
|
||||||
*/
|
|
||||||
export function navigateToCreateRequest(
|
|
||||||
navigate: NavigateFunction,
|
|
||||||
flowType: RequestFlowType = 'CUSTOM'
|
|
||||||
): void {
|
|
||||||
const route = flowType === 'DEALER_CLAIM'
|
|
||||||
? '/claim-management'
|
|
||||||
: '/new-request';
|
|
||||||
navigate(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a navigation handler function for request cards
|
|
||||||
* This can be used directly in onClick handlers
|
|
||||||
*/
|
|
||||||
export function createRequestNavigationHandler(
|
|
||||||
navigate: NavigateFunction
|
|
||||||
) {
|
|
||||||
return (requestId: string, requestTitle?: string, status?: string, request?: any) => {
|
|
||||||
navigateToRequest({
|
|
||||||
requestId,
|
|
||||||
requestTitle,
|
|
||||||
status,
|
|
||||||
request,
|
|
||||||
navigate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* Request Type Detection and Utilities
|
|
||||||
*
|
|
||||||
* Centralized utility for identifying request types and determining
|
|
||||||
* which flow/components to use for each request type.
|
|
||||||
*
|
|
||||||
* Supported Types:
|
|
||||||
* - CUSTOM: Standard custom requests
|
|
||||||
* - DEALER_CLAIM: Dealer claim management requests
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type RequestFlowType = 'CUSTOM' | 'DEALER_CLAIM';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a request is a Dealer Claim request
|
|
||||||
* Supports both old and new backend formats
|
|
||||||
*/
|
|
||||||
export function isDealerClaimRequest(request: any): boolean {
|
|
||||||
if (!request) return false;
|
|
||||||
|
|
||||||
// New format: Check workflowType
|
|
||||||
if (request.workflowType === 'CLAIM_MANAGEMENT' || request.workflowType === 'DEALER_CLAIM') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Old format: Check templateType (for backward compatibility)
|
|
||||||
if (request.templateType === 'claim-management' ||
|
|
||||||
request.template === 'claim-management' ||
|
|
||||||
request.templateType === 'dealer-claim') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check template name/code
|
|
||||||
if (request.templateName === 'Claim Management' ||
|
|
||||||
request.templateCode === 'CLAIM_MANAGEMENT' ||
|
|
||||||
request.templateCode === 'DEALER_CLAIM') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a request is a Custom request
|
|
||||||
*/
|
|
||||||
export function isCustomRequest(request: any): boolean {
|
|
||||||
if (!request) return false;
|
|
||||||
|
|
||||||
// If it's explicitly marked as custom
|
|
||||||
if (request.workflowType === 'CUSTOM' ||
|
|
||||||
request.workflowType === 'NON_TEMPLATIZED' ||
|
|
||||||
request.templateType === 'custom' ||
|
|
||||||
request.template === 'custom') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not a dealer claim, assume it's custom
|
|
||||||
// This handles legacy requests that don't have explicit type
|
|
||||||
if (!isDealerClaimRequest(request)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the flow type for a request
|
|
||||||
* Returns the appropriate RequestFlowType based on request properties
|
|
||||||
*/
|
|
||||||
export function getRequestFlowType(request: any): RequestFlowType {
|
|
||||||
if (isDealerClaimRequest(request)) {
|
|
||||||
return 'DEALER_CLAIM';
|
|
||||||
}
|
|
||||||
return 'CUSTOM';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the route path for a request detail page based on flow type
|
|
||||||
*/
|
|
||||||
export function getRequestDetailRoute(requestId: string, request?: any): string {
|
|
||||||
const flowType = request ? getRequestFlowType(request) : null;
|
|
||||||
|
|
||||||
// For now, all requests use the same route
|
|
||||||
// In the future, you can customize routes per flow type:
|
|
||||||
// if (flowType === 'DEALER_CLAIM') {
|
|
||||||
// return `/dealer-claim/${requestId}`;
|
|
||||||
// }
|
|
||||||
// return `/custom-request/${requestId}`;
|
|
||||||
|
|
||||||
return `/request/${requestId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the route path for creating a new request based on flow type
|
|
||||||
*/
|
|
||||||
export function getCreateRequestRoute(flowType: RequestFlowType): string {
|
|
||||||
switch (flowType) {
|
|
||||||
case 'DEALER_CLAIM':
|
|
||||||
return '/claim-management';
|
|
||||||
case 'CUSTOM':
|
|
||||||
default:
|
|
||||||
return '/new-request';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if request needs flow-specific UI components
|
|
||||||
*/
|
|
||||||
export function shouldUseFlowSpecificUI(request: any, flowType: RequestFlowType): boolean {
|
|
||||||
return getRequestFlowType(request) === flowType;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user