From 164d576ea0c111452366099e342296f18202ee03 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 2 Jan 2026 20:17:56 +0530 Subject: [PATCH] templates checked for the dealer claim and dashboard added for the dealer --- src/App.tsx | 45 +- .../AddApproverModal/AddApproverModal.tsx | 50 +- .../workNote/WorkNoteChat/WorkNoteChat.tsx | 6 +- .../CreateRequest/ApprovalWorkflowStep.tsx | 26 +- .../components/ClosedRequestsFilters.tsx | 172 +++++ src/custom/components/RequestsFilters.tsx | 161 ++++ .../components/UserAllRequestsFilters.tsx | 458 ++++++++++++ src/custom/index.ts | 5 + src/custom/pages/RequestDetail.tsx | 63 ++ .../DealerClosedRequestsFilters.tsx | 142 ++++ .../components/DealerRequestsFilters.tsx | 114 +++ .../DealerUserAllRequestsFilters.tsx | 390 ++++++++++ .../ClaimApproverSelectionStep.tsx | 71 +- .../ClaimManagementWizard.tsx | 59 ++ src/dealer-claim/index.ts | 8 + src/dealer-claim/pages/Dashboard.tsx | 687 ++++++++++++++++++ src/dealer-claim/pages/RequestDetail.tsx | 63 ++ src/flows.ts | 74 ++ src/hooks/useCreateRequestForm.ts | 6 +- src/pages/ClosedRequests/ClosedRequests.tsx | 45 +- src/pages/CreateRequest/CreateRequest.tsx | 5 + .../hooks/useCreateRequestHandlers.ts | 20 +- src/pages/OpenRequests/OpenRequests.tsx | 221 ++---- .../components/RequestDetailModals.tsx | 6 + .../components/tabs/WorkNotesTab.tsx | 6 + src/pages/Requests/UserAllRequests.tsx | 479 +++--------- src/services/dealerClaimApi.ts | 61 ++ src/utils/userFilterUtils.ts | 34 + 28 files changed, 2939 insertions(+), 538 deletions(-) create mode 100644 src/custom/components/ClosedRequestsFilters.tsx create mode 100644 src/custom/components/RequestsFilters.tsx create mode 100644 src/custom/components/UserAllRequestsFilters.tsx create mode 100644 src/dealer-claim/components/DealerClosedRequestsFilters.tsx create mode 100644 src/dealer-claim/components/DealerRequestsFilters.tsx create mode 100644 src/dealer-claim/components/DealerUserAllRequestsFilters.tsx create mode 100644 src/dealer-claim/pages/Dashboard.tsx create mode 100644 src/utils/userFilterUtils.ts diff --git a/src/App.tsx b/src/App.tsx index 7a938d7..6fd5816 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { SharedSummaryDetail } from '@/pages/SharedSummaries/SharedSummaryDetail import { WorkNotes } from '@/pages/WorkNotes'; import { CreateRequest } from '@/pages/CreateRequest'; import { ClaimManagementWizard } from '@/dealer-claim/components/request-creation/ClaimManagementWizard'; +import { DealerDashboard } from '@/dealer-claim/pages/Dashboard'; import { MyRequests } from '@/pages/MyRequests'; import { Requests } from '@/pages/Requests/Requests'; import { UserAllRequests } from '@/pages/Requests/UserAllRequests'; @@ -27,6 +28,7 @@ import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; import { AuthCallback } from '@/pages/Auth/AuthCallback'; import { createClaimRequest } from '@/services/dealerClaimApi'; import { ManagerSelectionModal } from '@/components/modals/ManagerSelectionModal'; +import { TokenManager } from '@/utils/tokenManager'; interface AppProps { onLogout?: () => void; @@ -48,6 +50,43 @@ function RequestsRoute({ onViewRequest }: { onViewRequest: (requestId: string) = } } +// Component to conditionally render Dashboard or DealerDashboard based on user job title +function DashboardRoute({ onNavigate, onNewRequest }: { onNavigate?: (page: string) => void; onNewRequest?: () => void }) { + const [isDealer, setIsDealer] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + try { + const userData = TokenManager.getUserData(); + setIsDealer(userData?.jobTitle === 'Dealer'); + } catch (error) { + console.error('[App] Error checking dealer status:', error); + setIsDealer(false); + } finally { + setIsLoading(false); + } + }, []); + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); + } + + // Render dealer-specific dashboard if user is a dealer + if (isDealer) { + return ; + } + + // Render regular dashboard for all other users + return ; +} + // Main Application Routes Component function AppRoutes({ onLogout }: AppProps) { const navigate = useNavigate(); @@ -573,12 +612,12 @@ function AppRoutes({ onLogout }: AppProps) { element={} /> - {/* Dashboard */} + {/* Dashboard - Conditionally renders DealerDashboard or regular Dashboard */} - + } /> @@ -587,7 +626,7 @@ function AppRoutes({ onLogout }: AppProps) { path="/dashboard" element={ - + } /> diff --git a/src/components/participant/AddApproverModal/AddApproverModal.tsx b/src/components/participant/AddApproverModal/AddApproverModal.tsx index 410a363..af5fc73 100644 --- a/src/components/participant/AddApproverModal/AddApproverModal.tsx +++ b/src/components/participant/AddApproverModal/AddApproverModal.tsx @@ -24,6 +24,8 @@ interface AddApproverModalProps { requestTitle?: string; existingParticipants?: Array<{ email: string; participantType: string; name?: string }>; currentLevels?: ApprovalLevelInfo[]; // Current approval levels + maxApprovalLevels?: number; // Maximum allowed approval levels from system policy + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } export function AddApproverModal({ @@ -31,7 +33,9 @@ export function AddApproverModal({ onClose, onConfirm, existingParticipants = [], - currentLevels = [] + currentLevels = [], + maxApprovalLevels, + onPolicyViolation }: AddApproverModalProps) { const [email, setEmail] = useState(''); const [tatHours, setTatHours] = useState(24); @@ -140,6 +144,36 @@ export function AddApproverModal({ return; } + // Validate against maxApprovalLevels policy + // Calculate the new total levels after adding this approver + // If inserting at a level that already exists, levels shift down, so total stays same + // If inserting at a new level (beyond current), total increases + const currentMaxLevel = currentLevels.length > 0 + ? Math.max(...currentLevels.map(l => l.levelNumber), 0) + : 0; + const newTotalLevels = selectedLevel > currentMaxLevel + ? selectedLevel // New level beyond current max + : currentMaxLevel + 1; // Existing level, shifts everything down, adds one more + + if (maxApprovalLevels && newTotalLevels > maxApprovalLevels) { + if (onPolicyViolation) { + onPolicyViolation([{ + type: 'Maximum Approval Levels Exceeded', + message: `Adding an approver at level ${selectedLevel} would result in ${newTotalLevels} approval levels, which exceeds the maximum allowed (${maxApprovalLevels}). Please remove an approver or contact your administrator.`, + currentValue: newTotalLevels, + maxValue: maxApprovalLevels + }]); + } else { + setValidationModal({ + open: true, + type: 'error', + email: '', + message: `Cannot add approver. This would exceed the maximum allowed approval levels (${maxApprovalLevels}). Current request has ${currentMaxLevel} level(s).` + }); + } + return; + } + // Check if user is already a participant const existingParticipant = existingParticipants.find( p => (p.email || '').toLowerCase() === emailToAdd @@ -394,6 +428,20 @@ export function AddApproverModal({ Add a new approver at a specific level. Existing approvers at and after the selected level will be shifted down.

+ {/* Max Approval Levels Note */} + {maxApprovalLevels && ( +
+

+ ℹ️ Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} + {currentLevels.length > 0 && ( + + ({Math.max(...currentLevels.map(l => l.levelNumber), 0)}/{maxApprovalLevels}) + + )} +

+
+ )} + {/* Current Levels Display */} {currentLevels.length > 0 && (
diff --git a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx index 972701a..f0db392 100644 --- a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx +++ b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx @@ -82,6 +82,8 @@ interface WorkNoteChatProps { isSpectator?: boolean; // Whether current user is a spectator (view-only) currentLevels?: any[]; // Current approval levels for add approver modal onAddApprover?: (email: string, tatHours: number, level: number) => Promise; // Callback to add approver + maxApprovalLevels?: number; // Maximum allowed approval levels from system policy + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; // Callback for policy violations } // All data is now fetched from backend - no hardcoded mock data @@ -142,7 +144,7 @@ const FileIcon = ({ type }: { type: string }) => { return ; }; -export function WorkNoteChat({ requestId, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted, isInitiator = false, isSpectator = false, currentLevels = [], onAddApprover }: WorkNoteChatProps) { +export function WorkNoteChat({ requestId, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted, isInitiator = false, isSpectator = false, currentLevels = [], onAddApprover, maxApprovalLevels, onPolicyViolation }: WorkNoteChatProps) { const routeParams = useParams<{ requestId: string }>(); const effectiveRequestId = requestId || routeParams.requestId || ''; const [message, setMessage] = useState(''); @@ -1815,6 +1817,8 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk requestTitle={requestInfo.title} existingParticipants={existingParticipants} currentLevels={currentLevels} + maxApprovalLevels={maxApprovalLevels} + onPolicyViolation={onPolicyViolation} /> )} diff --git a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx index 172ed03..10a1ffd 100644 --- a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx +++ b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx @@ -7,7 +7,7 @@ import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Users, Settings, Shield, User, CheckCircle, Minus, Plus, Info, Clock } from 'lucide-react'; -import { FormData } from '@/hooks/useCreateRequestForm'; +import { FormData, SystemPolicy } from '@/hooks/useCreateRequestForm'; import { useMultiUserSearch } from '@/hooks/useUserSearch'; import { ensureUserExists } from '@/services/userApi'; @@ -15,6 +15,8 @@ interface ApprovalWorkflowStepProps { formData: FormData; updateFormData: (field: keyof FormData, value: any) => void; onValidationError: (error: { type: string; email: string; message: string }) => void; + systemPolicy: SystemPolicy; + onPolicyViolation: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } /** @@ -33,7 +35,9 @@ interface ApprovalWorkflowStepProps { export function ApprovalWorkflowStep({ formData, updateFormData, - onValidationError + onValidationError, + systemPolicy, + onPolicyViolation }: ApprovalWorkflowStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); @@ -218,17 +222,29 @@ export function ApprovalWorkflowStep({ size="sm" onClick={() => { const currentCount = formData.approverCount || 1; - const newCount = Math.min(10, currentCount + 1); + const newCount = currentCount + 1; + + // Validate against system policy + if (newCount > systemPolicy.maxApprovalLevels) { + onPolicyViolation([{ + type: 'Maximum Approval Levels Exceeded', + message: `Cannot add more than ${systemPolicy.maxApprovalLevels} approval levels. Please remove an approver level or contact your administrator.`, + currentValue: newCount, + maxValue: systemPolicy.maxApprovalLevels + }]); + return; + } + updateFormData('approverCount', newCount); }} - disabled={(formData.approverCount || 1) >= 10} + disabled={(formData.approverCount || 1) >= systemPolicy.maxApprovalLevels} data-testid="approval-workflow-increase-count" >

- Maximum 10 approvers allowed. Each approver will review sequentially. + Maximum {systemPolicy.maxApprovalLevels} approver{systemPolicy.maxApprovalLevels !== 1 ? 's' : ''} allowed. Each approver will review sequentially.

diff --git a/src/custom/components/ClosedRequestsFilters.tsx b/src/custom/components/ClosedRequestsFilters.tsx new file mode 100644 index 0000000..ffa98ac --- /dev/null +++ b/src/custom/components/ClosedRequestsFilters.tsx @@ -0,0 +1,172 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Filter, Search, SortAsc, SortDesc, Flame, Target, CheckCircle, XCircle, X } from 'lucide-react'; + +interface ClosedRequestsFiltersProps { + searchTerm: string; + priorityFilter: string; + statusFilter: string; + templateTypeFilter: string; + sortBy: 'created' | 'due' | 'priority'; + sortOrder: 'asc' | 'desc'; + activeFiltersCount: number; + onSearchChange: (value: string) => void; + onPriorityChange: (value: string) => void; + onStatusChange: (value: string) => void; + onTemplateTypeChange: (value: string) => void; + onSortByChange: (value: 'created' | 'due' | 'priority') => void; + onSortOrderChange: () => void; + onClearFilters: () => void; +} + +/** + * Standard Closed Requests Filters Component + * + * Used for regular users (non-dealers). + * Includes: Search, Priority, Status (Closure Type), Template Type, and Sort filters. + */ +export function StandardClosedRequestsFilters({ + searchTerm, + priorityFilter, + statusFilter, + templateTypeFilter, + sortBy, + sortOrder, + activeFiltersCount, + onSearchChange, + onPriorityChange, + onStatusChange, + onTemplateTypeChange, + onSortByChange, + onSortOrderChange, + onClearFilters, +}: ClosedRequestsFiltersProps) { + return ( + + +
+
+
+ +
+
+ Filters & Search + + {activeFiltersCount > 0 && ( + + {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active + + )} + +
+
+ {activeFiltersCount > 0 && ( + + )} +
+
+ +
+
+ + onSearchChange(e.target.value)} + className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white transition-colors" + data-testid="closed-requests-search" + /> +
+ + + + + + + +
+ + + +
+
+
+
+ ); +} + diff --git a/src/custom/components/RequestsFilters.tsx b/src/custom/components/RequestsFilters.tsx new file mode 100644 index 0000000..7ad671d --- /dev/null +++ b/src/custom/components/RequestsFilters.tsx @@ -0,0 +1,161 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Filter, Search, SortAsc, SortDesc, X, Flame, Target } from 'lucide-react'; + +interface RequestsFiltersProps { + searchTerm: string; + statusFilter: string; + priorityFilter: string; + templateTypeFilter: string; + sortBy: 'created' | 'due' | 'priority' | 'sla'; + sortOrder: 'asc' | 'desc'; + onSearchChange: (value: string) => void; + onStatusFilterChange: (value: string) => void; + onPriorityFilterChange: (value: string) => void; + onTemplateTypeFilterChange: (value: string) => void; + onSortByChange: (value: 'created' | 'due' | 'priority' | 'sla') => void; + onSortOrderChange: (value: 'asc' | 'desc') => void; + onClearFilters: () => void; + activeFiltersCount: number; +} + +/** + * Standard Requests Filters Component + * + * Used for regular users (non-dealers). + * Includes: Search, Status, Priority, Template Type, and Sort filters. + */ +export function StandardRequestsFilters({ + searchTerm, + statusFilter, + priorityFilter, + templateTypeFilter, + sortBy, + sortOrder, + onSearchChange, + onStatusFilterChange, + onPriorityFilterChange, + onTemplateTypeFilterChange, + onSortByChange, + onSortOrderChange, + onClearFilters, + activeFiltersCount, +}: RequestsFiltersProps) { + return ( + + +
+
+
+ +
+
+ Filters & Search + + {activeFiltersCount > 0 && ( + + {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active + + )} + +
+
+ {activeFiltersCount > 0 && ( + + )} +
+
+ + {/* Standard filters - Search, Status, Priority, Template Type, and Sort */} +
+
+ + onSearchChange(e.target.value)} + className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition-colors" + /> +
+ + + + + + + +
+ + + +
+
+
+
+ ); +} + diff --git a/src/custom/components/UserAllRequestsFilters.tsx b/src/custom/components/UserAllRequestsFilters.tsx new file mode 100644 index 0000000..478d335 --- /dev/null +++ b/src/custom/components/UserAllRequestsFilters.tsx @@ -0,0 +1,458 @@ +/** + * Standard User All Requests Filters Component + * + * Full filters for regular users (non-dealers). + * Includes: Search, Status, Priority, Template Type, Department, SLA Compliance, + * Initiator, Approver, and Date Range filters. + */ + +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react'; +import { format } from 'date-fns'; +import type { DateRange } from '@/services/dashboard.service'; + +interface StandardUserAllRequestsFiltersProps { + // Filters + searchTerm: string; + statusFilter: string; + priorityFilter: string; + templateTypeFilter: string; + departmentFilter: string; + slaComplianceFilter: string; + initiatorFilter: string; + approverFilter: string; + approverFilterType: 'current' | 'any'; + dateRange: DateRange; + customStartDate?: Date; + customEndDate?: Date; + showCustomDatePicker: boolean; + + // Departments + departments: string[]; + loadingDepartments: boolean; + + // State for user search + initiatorSearch: { + selectedUser: { userId: string; email: string; displayName?: string } | null; + searchQuery: string; + searchResults: Array<{ userId: string; email: string; displayName?: string }>; + showResults: boolean; + handleSearch: (query: string) => void; + handleSelect: (user: { userId: string; email: string; displayName?: string }) => void; + handleClear: () => void; + setShowResults: (show: boolean) => void; + }; + + approverSearch: { + selectedUser: { userId: string; email: string; displayName?: string } | null; + searchQuery: string; + searchResults: Array<{ userId: string; email: string; displayName?: string }>; + showResults: boolean; + handleSearch: (query: string) => void; + handleSelect: (user: { userId: string; email: string; displayName?: string }) => void; + handleClear: () => void; + setShowResults: (show: boolean) => void; + }; + + // Actions + onSearchChange: (value: string) => void; + onStatusChange: (value: string) => void; + onPriorityChange: (value: string) => void; + onTemplateTypeChange: (value: string) => void; + onDepartmentChange: (value: string) => void; + onSlaComplianceChange: (value: string) => void; + onInitiatorChange?: (value: string) => void; + onApproverChange?: (value: string) => void; + onApproverTypeChange?: (value: 'current' | 'any') => void; + onDateRangeChange: (value: DateRange) => void; + onCustomStartDateChange?: (date: Date | undefined) => void; + onCustomEndDateChange?: (date: Date | undefined) => void; + onShowCustomDatePickerChange?: (show: boolean) => void; + onApplyCustomDate?: () => void; + onClearFilters: () => void; + + // Computed + hasActiveFilters: boolean; +} + +export function StandardUserAllRequestsFilters({ + searchTerm, + statusFilter, + priorityFilter, + templateTypeFilter, + departmentFilter, + slaComplianceFilter, + initiatorFilter: _initiatorFilter, + approverFilter, + approverFilterType, + dateRange, + customStartDate, + customEndDate, + showCustomDatePicker, + departments, + loadingDepartments, + initiatorSearch, + approverSearch, + onSearchChange, + onStatusChange, + onPriorityChange, + onTemplateTypeChange, + onDepartmentChange, + onSlaComplianceChange, + onInitiatorChange: _onInitiatorChange, + onApproverChange: _onApproverChange, + onApproverTypeChange, + onDateRangeChange, + onCustomStartDateChange, + onCustomEndDateChange, + onShowCustomDatePickerChange, + onApplyCustomDate, + onClearFilters, + hasActiveFilters, +}: StandardUserAllRequestsFiltersProps) { + return ( + + +
+
+
+ +

Advanced Filters

+ {hasActiveFilters && ( + + Active + + )} +
+ {hasActiveFilters && ( + + )} +
+ + + + {/* Primary Filters */} +
+
+ + onSearchChange(e.target.value)} + className="pl-10 h-10" + data-testid="search-input" + /> +
+ + + + + + + + + + +
+ + {/* User Filters - Initiator and Approver */} +
+ {/* Initiator Filter */} +
+ +
+ {initiatorSearch.selectedUser ? ( +
+ + {initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email} + + +
+ ) : ( + <> + initiatorSearch.handleSearch(e.target.value)} + onFocus={() => { + if (initiatorSearch.searchResults.length > 0) { + initiatorSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)} + className="h-10" + data-testid="initiator-search-input" + /> + {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && ( +
+ {initiatorSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+ + {/* Approver Filter */} +
+
+ + {approverFilter !== 'all' && onApproverTypeChange && ( + + )} +
+
+ {approverSearch.selectedUser ? ( +
+ + {approverSearch.selectedUser.displayName || approverSearch.selectedUser.email} + + +
+ ) : ( + <> + approverSearch.handleSearch(e.target.value)} + onFocus={() => { + if (approverSearch.searchResults.length > 0) { + approverSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)} + className="h-10" + data-testid="approver-search-input" + /> + {approverSearch.showResults && approverSearch.searchResults.length > 0 && ( +
+ {approverSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+
+ + {/* Date Range Filter */} +
+ + + + {dateRange === 'custom' && ( + + + + + +
+
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + onCustomStartDateChange?.(date); + if (customEndDate && date > customEndDate) { + onCustomEndDateChange?.(date); + } + } else { + onCustomStartDateChange?.(undefined); + } + }} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + onCustomEndDateChange?.(date); + if (customStartDate && date < customStartDate) { + onCustomStartDateChange?.(date); + } + } else { + onCustomEndDateChange?.(undefined); + } + }} + min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+
+ + +
+
+
+
+ )} +
+
+
+
+ ); +} + diff --git a/src/custom/index.ts b/src/custom/index.ts index 51fe58d..76a75eb 100644 --- a/src/custom/index.ts +++ b/src/custom/index.ts @@ -23,5 +23,10 @@ export { CreateRequest as CustomCreateRequest } from './components/request-creat // Request Detail Screen (Complete standalone screen) export { CustomRequestDetail } from './pages/RequestDetail'; +// Filters +export { StandardRequestsFilters } from './components/RequestsFilters'; +export { StandardClosedRequestsFilters } from './components/ClosedRequestsFilters'; +export { StandardUserAllRequestsFilters } from './components/UserAllRequestsFilters'; + // Re-export types export type { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types'; diff --git a/src/custom/pages/RequestDetail.tsx b/src/custom/pages/RequestDetail.tsx index 7b910ba..a24d505 100644 --- a/src/custom/pages/RequestDetail.tsx +++ b/src/custom/pages/RequestDetail.tsx @@ -37,6 +37,8 @@ import { useDocumentUpload } from '@/hooks/useDocumentUpload'; import { useConclusionRemark } from '@/hooks/useConclusionRemark'; import { useModalManager } from '@/hooks/useModalManager'; import { downloadDocument } from '@/services/workflowApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; // Custom Request Components (import from index to get properly aliased exports) import { CustomOverviewTab, CustomWorkflowTab } from '../index'; @@ -112,6 +114,24 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq const [showPauseModal, setShowPauseModal] = useState(false); const [showResumeModal, setShowResumeModal] = useState(false); const [showRetriggerModal, setShowRetriggerModal] = useState(false); + const [systemPolicy, setSystemPolicy] = useState<{ + maxApprovalLevels: number; + maxParticipants: number; + allowSpectators: boolean; + maxSpectators: number; + }>({ + maxApprovalLevels: 10, + maxParticipants: 50, + allowSpectators: true, + maxSpectators: 20 + }); + const [policyViolationModal, setPolicyViolationModal] = useState<{ + open: boolean; + violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>; + }>({ + open: false, + violations: [] + }); const { user } = useAuth(); // Custom hooks @@ -179,6 +199,32 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq handleFinalizeConclusion, } = useConclusionRemark(request, requestIdentifier, isInitiator, refreshDetails, onBack, setActionStatus, setShowActionStatusModal); + // Load system policy on mount + useEffect(() => { + const loadSystemPolicy = async () => { + try { + const systemSettingsConfigs = await getPublicConfigurations('SYSTEM_SETTINGS'); + const workflowSharingConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const allConfigs = [...systemSettingsConfigs, ...workflowSharingConfigs]; + const configMap: Record = {}; + allConfigs.forEach((c: AdminConfiguration) => { + configMap[c.configKey] = c.configValue; + }); + + setSystemPolicy({ + maxApprovalLevels: parseInt(configMap['MAX_APPROVAL_LEVELS'] || '10'), + maxParticipants: parseInt(configMap['MAX_PARTICIPANTS_PER_REQUEST'] || '50'), + allowSpectators: configMap['ALLOW_ADD_SPECTATOR']?.toLowerCase() === 'true', + maxSpectators: parseInt(configMap['MAX_SPECTATORS_PER_REQUEST'] || '20') + }); + } catch (error) { + console.error('Failed to load system policy:', error); + } + }; + + loadSystemPolicy(); + }, []); + // Auto-switch tab when URL query parameter changes useEffect(() => { const urlParams = new URLSearchParams(window.location.search); @@ -521,6 +567,8 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq isSpectator={isSpectator} currentLevels={currentLevels} onAddApprover={handleAddApprover} + maxApprovalLevels={systemPolicy.maxApprovalLevels} + onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })} /> @@ -610,6 +658,8 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq actionStatus={actionStatus} existingParticipants={existingParticipants} currentLevels={currentLevels} + maxApprovalLevels={systemPolicy.maxApprovalLevels} + onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })} setShowApproveModal={setShowApproveModal} setShowRejectModal={setShowRejectModal} setShowAddApproverModal={setShowAddApproverModal} @@ -628,6 +678,19 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq downloadDocument={downloadDocument} documentPolicy={documentPolicy} /> + + {/* Policy Violation Modal */} + setPolicyViolationModal({ open: false, violations: [] })} + violations={policyViolationModal.violations} + policyDetails={{ + maxApprovalLevels: systemPolicy.maxApprovalLevels, + maxParticipants: systemPolicy.maxParticipants, + allowSpectators: systemPolicy.allowSpectators, + maxSpectators: systemPolicy.maxSpectators, + }} + /> ); } diff --git a/src/dealer-claim/components/DealerClosedRequestsFilters.tsx b/src/dealer-claim/components/DealerClosedRequestsFilters.tsx new file mode 100644 index 0000000..a460be1 --- /dev/null +++ b/src/dealer-claim/components/DealerClosedRequestsFilters.tsx @@ -0,0 +1,142 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Filter, Search, SortAsc, SortDesc, X, CheckCircle, XCircle } from 'lucide-react'; + +interface DealerClosedRequestsFiltersProps { + searchTerm: string; + statusFilter?: string; + priorityFilter?: string; + templateTypeFilter?: string; + sortBy: 'created' | 'due' | 'priority'; + sortOrder: 'asc' | 'desc'; + onSearchChange: (value: string) => void; + onStatusChange?: (value: string) => void; + onPriorityChange?: (value: string) => void; + onTemplateTypeChange?: (value: string) => void; + onSortByChange: (value: 'created' | 'due' | 'priority') => void; + onSortOrderChange: () => void; + onClearFilters: () => void; + activeFiltersCount: number; +} + +/** + * Dealer Closed Requests Filters Component + * + * Simplified filters for dealer users viewing closed requests. + * Only includes: Search, Status (closure type), and Sort filters. + * Removes: Priority and Template Type filters. + */ +export function DealerClosedRequestsFilters({ + searchTerm, + statusFilter = 'all', + sortBy, + sortOrder, + onSearchChange, + onStatusChange, + onSortByChange, + onSortOrderChange, + onClearFilters, + activeFiltersCount, + ...rest // Accept but ignore other props for interface compatibility +}: DealerClosedRequestsFiltersProps) { + void rest; // Explicitly mark as unused + return ( + + +
+
+
+ +
+
+ Filters & Search + + {activeFiltersCount > 0 && ( + + {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active + + )} + +
+
+ {activeFiltersCount > 0 && ( + + )} +
+
+ + {/* Dealer-specific filters - Search, Status (Closure Type), and Sort */} +
+
+ + onSearchChange(e.target.value)} + className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition-colors" + data-testid="dealer-closed-requests-search" + /> +
+ + {onStatusChange && ( + + )} + +
+ + + +
+
+
+
+ ); +} + diff --git a/src/dealer-claim/components/DealerRequestsFilters.tsx b/src/dealer-claim/components/DealerRequestsFilters.tsx new file mode 100644 index 0000000..0c4b4b5 --- /dev/null +++ b/src/dealer-claim/components/DealerRequestsFilters.tsx @@ -0,0 +1,114 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Filter, Search, SortAsc, SortDesc, X } from 'lucide-react'; + +interface DealerRequestsFiltersProps { + searchTerm: string; + statusFilter?: string; + priorityFilter?: string; + templateTypeFilter?: string; + sortBy: 'created' | 'due' | 'priority' | 'sla'; + sortOrder: 'asc' | 'desc'; + onSearchChange: (value: string) => void; + onStatusFilterChange?: (value: string) => void; + onPriorityFilterChange?: (value: string) => void; + onTemplateTypeFilterChange?: (value: string) => void; + onSortByChange: (value: 'created' | 'due' | 'priority' | 'sla') => void; + onSortOrderChange: (value: 'asc' | 'desc') => void; + onClearFilters: () => void; + activeFiltersCount: number; +} + +/** + * Dealer Requests Filters Component + * + * Simplified filters for dealer users. + * Only includes: Search and Sort filters (no status, priority, or template type). + */ +export function DealerRequestsFilters({ + searchTerm, + sortBy, + sortOrder, + onSearchChange, + onSortByChange, + onSortOrderChange, + onClearFilters, + activeFiltersCount, + ...rest // Accept but ignore other props for interface compatibility +}: DealerRequestsFiltersProps) { + void rest; // Explicitly mark as unused + return ( + + +
+
+
+ +
+
+ Filters & Search + + {activeFiltersCount > 0 && ( + + {activeFiltersCount} filter{activeFiltersCount > 1 ? 's' : ''} active + + )} + +
+
+ {activeFiltersCount > 0 && ( + + )} +
+
+ + {/* Dealer-specific filters - Only Search and Sort */} +
+
+ + onSearchChange(e.target.value)} + className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition-colors" + /> +
+ +
+ + + +
+
+
+
+ ); +} + diff --git a/src/dealer-claim/components/DealerUserAllRequestsFilters.tsx b/src/dealer-claim/components/DealerUserAllRequestsFilters.tsx new file mode 100644 index 0000000..b066e42 --- /dev/null +++ b/src/dealer-claim/components/DealerUserAllRequestsFilters.tsx @@ -0,0 +1,390 @@ +/** + * Dealer User All Requests Filters Component + * + * Simplified filters for dealer users viewing their all requests. + * Only includes: Search, Status, Initiator, Approver, and Date Range filters. + * Removes: Priority, Template Type, Department, and SLA Compliance filters. + */ + +import { Card, CardContent } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Label } from '@/components/ui/label'; +import { Separator } from '@/components/ui/separator'; +import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react'; +import { format } from 'date-fns'; +import type { DateRange } from '@/services/dashboard.service'; + +interface DealerUserAllRequestsFiltersProps { + // Filters + searchTerm: string; + statusFilter: string; + priorityFilter?: string; + templateTypeFilter?: string; + departmentFilter?: string; + slaComplianceFilter?: string; + initiatorFilter: string; + approverFilter: string; + approverFilterType: 'current' | 'any'; + dateRange: DateRange; + customStartDate?: Date; + customEndDate?: Date; + showCustomDatePicker: boolean; + + // State for user search + initiatorSearch: { + selectedUser: { userId: string; email: string; displayName?: string } | null; + searchQuery: string; + searchResults: Array<{ userId: string; email: string; displayName?: string }>; + showResults: boolean; + handleSearch: (query: string) => void; + handleSelect: (user: { userId: string; email: string; displayName?: string }) => void; + handleClear: () => void; + setShowResults: (show: boolean) => void; + }; + + approverSearch: { + selectedUser: { userId: string; email: string; displayName?: string } | null; + searchQuery: string; + searchResults: Array<{ userId: string; email: string; displayName?: string }>; + showResults: boolean; + handleSearch: (query: string) => void; + handleSelect: (user: { userId: string; email: string; displayName?: string }) => void; + handleClear: () => void; + setShowResults: (show: boolean) => void; + }; + + // Actions + onSearchChange: (value: string) => void; + onStatusChange: (value: string) => void; + onInitiatorChange?: (value: string) => void; + onApproverChange?: (value: string) => void; + onApproverTypeChange?: (value: 'current' | 'any') => void; + onDateRangeChange: (value: DateRange) => void; + onCustomStartDateChange?: (date: Date | undefined) => void; + onCustomEndDateChange?: (date: Date | undefined) => void; + onShowCustomDatePickerChange?: (show: boolean) => void; + onApplyCustomDate?: () => void; + onClearFilters: () => void; + + // Computed + hasActiveFilters: boolean; +} + +export function DealerUserAllRequestsFilters({ + searchTerm, + statusFilter, + initiatorFilter, + approverFilter, + approverFilterType, + dateRange, + customStartDate, + customEndDate, + showCustomDatePicker, + initiatorSearch, + approverSearch, + onSearchChange, + onStatusChange, + onInitiatorChange, + onApproverChange, + onApproverTypeChange, + onDateRangeChange, + onCustomStartDateChange, + onCustomEndDateChange, + onShowCustomDatePickerChange, + onApplyCustomDate, + onClearFilters, + hasActiveFilters, + ...rest // Accept but ignore other props for interface compatibility +}: DealerUserAllRequestsFiltersProps) { + void rest; // Explicitly mark as unused + return ( + + +
+
+
+ +

Filters

+ {hasActiveFilters && ( + + Active + + )} +
+ {hasActiveFilters && ( + + )} +
+ + + + {/* Primary Filters - Only Search and Status for dealers */} +
+
+ + onSearchChange(e.target.value)} + className="pl-10 h-10" + data-testid="dealer-search-input" + /> +
+ + +
+ + {/* User Filters - Initiator and Approver */} +
+ {/* Initiator Filter */} +
+ +
+ {initiatorSearch.selectedUser ? ( +
+ + {initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email} + + +
+ ) : ( + <> + initiatorSearch.handleSearch(e.target.value)} + onFocus={() => { + if (initiatorSearch.searchResults.length > 0) { + initiatorSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)} + className="h-10" + data-testid="dealer-initiator-search-input" + /> + {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && ( +
+ {initiatorSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+ + {/* Approver Filter */} +
+
+ + {approverFilter !== 'all' && onApproverTypeChange && ( + + )} +
+
+ {approverSearch.selectedUser ? ( +
+ + {approverSearch.selectedUser.displayName || approverSearch.selectedUser.email} + + +
+ ) : ( + <> + approverSearch.handleSearch(e.target.value)} + onFocus={() => { + if (approverSearch.searchResults.length > 0) { + approverSearch.setShowResults(true); + } + }} + onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)} + className="h-10" + data-testid="dealer-approver-search-input" + /> + {approverSearch.showResults && approverSearch.searchResults.length > 0 && ( +
+ {approverSearch.searchResults.map((user) => ( + + ))} +
+ )} + + )} +
+
+
+ + {/* Date Range Filter */} +
+ + + + {dateRange === 'custom' && ( + + + + + +
+
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + onCustomStartDateChange?.(date); + if (customEndDate && date > customEndDate) { + onCustomEndDateChange?.(date); + } + } else { + onCustomStartDateChange?.(undefined); + } + }} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+ + { + const date = e.target.value ? new Date(e.target.value) : undefined; + if (date) { + onCustomEndDateChange?.(date); + if (customStartDate && date < customStartDate) { + onCustomStartDateChange?.(date); + } + } else { + onCustomEndDateChange?.(undefined); + } + }} + min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined} + max={format(new Date(), 'yyyy-MM-dd')} + className="w-full" + /> +
+
+
+ + +
+
+
+
+ )} +
+
+
+
+ ); +} + diff --git a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx index e98e7e6..9c9c86f 100644 --- a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx +++ b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx @@ -54,6 +54,8 @@ interface ClaimApproverSelectionStepProps { currentUserId?: string; currentUserName?: string; onValidate?: (isValid: boolean) => void; + maxApprovalLevels?: number; + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } export function ClaimApproverSelectionStep({ @@ -64,6 +66,8 @@ export function ClaimApproverSelectionStep({ currentUserId = '', currentUserName = '', onValidate, + maxApprovalLevels, + onPolicyViolation, }: ClaimApproverSelectionStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); @@ -560,6 +564,30 @@ export function ClaimApproverSelectionStep({ // Calculate insert level based on current shifted level const insertLevel = currentLevelAfter + 1; + // Validate max approval levels + if (maxApprovalLevels) { + // Calculate total levels after adding the new approver + // After shifting, we'll have the same number of unique levels + 1 (the new approver) + const currentUniqueLevels = new Set(approvers.map((a: ClaimApprover) => a.level)).size; + const newTotalLevels = currentUniqueLevels + 1; + + if (newTotalLevels > maxApprovalLevels) { + const violations = [{ + type: 'max_approval_levels', + message: `Adding this approver would create ${newTotalLevels} approval levels, which exceeds the maximum allowed (${maxApprovalLevels}). Please remove some approvers before adding a new one.`, + currentValue: newTotalLevels, + maxValue: maxApprovalLevels + }]; + + if (onPolicyViolation) { + onPolicyViolation(violations); + } else { + toast.error(violations[0]?.message || 'Maximum approval levels exceeded'); + } + return; + } + } + // If user was NOT selected via @ search, validate against Okta if (!selectedAddApproverUser || selectedAddApproverUser.email.toLowerCase() !== emailToAdd) { try { @@ -728,6 +756,19 @@ export function ClaimApproverSelectionStep({ Some steps are pre-filled (Dealer, Initiator, System). You need to assign approvers for "Department Lead Approval" only. Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final. + {maxApprovalLevels && ( + + Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} + {(() => { + const approvers = formData.approvers || []; + const allLevels = new Set(approvers.map((a: ClaimApprover) => a.level)); + const currentCount = allLevels.size; + return currentCount > 0 ? ( + ({currentCount}/{maxApprovalLevels}) + ) : null; + })()} + + )} @@ -745,7 +786,20 @@ export function ClaimApproverSelectionStep({ {/* Add Additional Approver Button */} -
+
+ {maxApprovalLevels && ( +

+ Max: {maxApprovalLevels} level{maxApprovalLevels !== 1 ? 's' : ''} + {(() => { + const approvers = formData.approvers || []; + const allLevels = new Set(approvers.map((a: ClaimApprover) => a.level)); + const currentCount = allLevels.size; + return currentCount > 0 ? ( + ({currentCount}/{maxApprovalLevels}) + ) : null; + })()} +

+ )}
{/* TAT Input */} diff --git a/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx b/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx index 7fc5543..1520d7b 100644 --- a/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx +++ b/src/dealer-claim/components/request-creation/ClaimManagementWizard.tsx @@ -32,6 +32,8 @@ import { toast } from 'sonner'; import { getAllDealers as fetchDealersFromAPI, verifyDealerLogin, type DealerInfo } from '@/services/dealerApi'; import { ClaimApproverSelectionStep } from './ClaimApproverSelectionStep'; import { useAuth } from '@/contexts/AuthContext'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; // CLAIM_STEPS definition (same as in ClaimApproverSelectionStep) const CLAIM_STEPS = [ @@ -82,6 +84,48 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar const dealerSearchTimer = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const submitTimeoutRef = useRef(null); + + // System policy state + const [systemPolicy, setSystemPolicy] = useState({ + maxApprovalLevels: 10, + maxParticipants: 50, + allowSpectators: true, + maxSpectators: 20 + }); + + const [policyViolationModal, setPolicyViolationModal] = useState<{ + open: boolean; + violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>; + }>({ + open: false, + violations: [] + }); + + // Load system policy on mount + useEffect(() => { + const loadSystemPolicy = async () => { + try { + const systemSettingsConfigs = await getPublicConfigurations('SYSTEM_SETTINGS'); + const workflowSharingConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const allConfigs = [...systemSettingsConfigs, ...workflowSharingConfigs]; + const configMap: Record = {}; + allConfigs.forEach((c: AdminConfiguration) => { + configMap[c.configKey] = c.configValue; + }); + + setSystemPolicy({ + maxApprovalLevels: parseInt(configMap['MAX_APPROVAL_LEVELS'] || '10'), + maxParticipants: parseInt(configMap['MAX_PARTICIPANTS_PER_REQUEST'] || '50'), + allowSpectators: configMap['ALLOW_ADD_SPECTATOR']?.toLowerCase() === 'true', + maxSpectators: parseInt(configMap['MAX_SPECTATORS_PER_REQUEST'] || '20') + }); + } catch (error) { + console.error('Failed to load system policy:', error); + } + }; + + loadSystemPolicy(); + }, []); // Cleanup timeout on unmount useEffect(() => { @@ -699,6 +743,8 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar ? `${(user as any).firstName} ${(user as any).lastName}`.trim() : (user as any)?.email || 'User') } + maxApprovalLevels={systemPolicy.maxApprovalLevels} + onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })} /> ); @@ -1052,6 +1098,19 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar )}
+ + {/* Policy Violation Modal */} + setPolicyViolationModal({ open: false, violations: [] })} + violations={policyViolationModal.violations} + policyDetails={{ + maxApprovalLevels: systemPolicy.maxApprovalLevels, + maxParticipants: systemPolicy.maxParticipants, + allowSpectators: systemPolicy.allowSpectators, + maxSpectators: systemPolicy.maxSpectators, + }} + /> ); } diff --git a/src/dealer-claim/index.ts b/src/dealer-claim/index.ts index fcee7fe..9359845 100644 --- a/src/dealer-claim/index.ts +++ b/src/dealer-claim/index.ts @@ -30,5 +30,13 @@ export { ClaimManagementWizard } from './components/request-creation/ClaimManage // Request Detail Screen (Complete standalone screen) export { DealerClaimRequestDetail } from './pages/RequestDetail'; +// Dashboard +export { DealerDashboard } from './pages/Dashboard'; + +// Filters +export { DealerRequestsFilters } from './components/DealerRequestsFilters'; +export { DealerClosedRequestsFilters } from './components/DealerClosedRequestsFilters'; +export { DealerUserAllRequestsFilters } from './components/DealerUserAllRequestsFilters'; + // Re-export types export type { RequestDetailProps } from '@/pages/RequestDetail/types/requestDetail.types'; diff --git a/src/dealer-claim/pages/Dashboard.tsx b/src/dealer-claim/pages/Dashboard.tsx new file mode 100644 index 0000000..417e0a4 --- /dev/null +++ b/src/dealer-claim/pages/Dashboard.tsx @@ -0,0 +1,687 @@ +import { useEffect, useState, useMemo } from 'react'; +import { Shield, Clock, FileText, ChartColumn, ChartPie, Activity, Target, DollarSign, Zap, Package, TrendingUp, TrendingDown, CircleCheckBig, CircleX, CreditCard, TriangleAlert } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; +import { getDealerDashboard, type DashboardKPIs as DashboardKPIsType, type CategoryData as CategoryDataType } from '@/services/dealerClaimApi'; +import { RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; + +// Use types from dealerClaimApi +type DashboardKPIs = DashboardKPIsType; +type CategoryData = CategoryDataType; + +interface DashboardProps { + onNavigate?: (page: string) => void; + onNewRequest?: () => void; +} + +export function DealerDashboard({ onNavigate, onNewRequest: _onNewRequest }: DashboardProps) { + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [kpis, setKpis] = useState({ + totalClaims: 0, + totalValue: 0, + approved: 0, + rejected: 0, + pending: 0, + credited: 0, + pendingCredit: 0, + approvedValue: 0, + rejectedValue: 0, + pendingValue: 0, + creditedValue: 0, + pendingCreditValue: 0, + }); + const [categoryData, setCategoryData] = useState([]); + const [dateRange, _setDateRange] = useState('all'); + const [startDate, _setStartDate] = useState(); + const [endDate, _setEndDate] = useState(); + + const fetchDashboardData = async (isRefresh = false) => { + try { + if (isRefresh) { + setRefreshing(true); + } else { + setLoading(true); + } + + // Fetch dealer claims dashboard data + const data = await getDealerDashboard( + dateRange || 'all', + startDate, + endDate + ); + + setKpis(data.kpis || { + totalClaims: 0, + totalValue: 0, + approved: 0, + rejected: 0, + pending: 0, + credited: 0, + pendingCredit: 0, + approvedValue: 0, + rejectedValue: 0, + pendingValue: 0, + creditedValue: 0, + pendingCreditValue: 0, + }); + + setCategoryData(data.categoryData || []); + } catch (error: any) { + console.error('[DealerDashboard] Error fetching data:', error); + toast.error('Failed to load dashboard data. Please try again later.'); + // Reset to empty state on error + setKpis({ + totalClaims: 0, + totalValue: 0, + approved: 0, + rejected: 0, + pending: 0, + credited: 0, + pendingCredit: 0, + approvedValue: 0, + rejectedValue: 0, + pendingValue: 0, + creditedValue: 0, + pendingCreditValue: 0, + }); + setCategoryData([]); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchDashboardData(); + }, []); + + const formatCurrency = (amount: number, showExactRupees = false) => { + // Handle null, undefined, or invalid values + if (amount == null || isNaN(amount)) { + return '₹0'; + } + + // Convert to number if it's a string + const numAmount = typeof amount === 'string' ? parseFloat(amount) : Number(amount); + + // Handle zero or negative values + if (numAmount <= 0) { + return '₹0'; + } + + // If showExactRupees is true or amount is less than 10,000, show exact rupees + if (showExactRupees || numAmount < 10000) { + return `₹${Math.round(numAmount).toLocaleString('en-IN')}`; + } + + if (numAmount >= 100000) { + return `₹${(numAmount / 100000).toFixed(1)}L`; + } + if (numAmount >= 1000) { + return `₹${(numAmount / 1000).toFixed(1)}K`; + } + // Show exact rupee amount for amounts less than 1000 (e.g., ₹100, ₹200, ₹999) + return `₹${Math.round(numAmount).toLocaleString('en-IN')}`; + }; + + const formatNumber = (num: number) => { + return num.toLocaleString('en-IN'); + }; + + const calculateApprovalRate = () => { + if (kpis.totalClaims === 0) return 0; + return ((kpis.approved / kpis.totalClaims) * 100).toFixed(1); + }; + + const calculateCreditRate = () => { + if (kpis.approved === 0) return 0; + return ((kpis.credited / kpis.approved) * 100).toFixed(1); + }; + + // Prepare data for pie chart (Distribution by Activity Type) + const distributionData = useMemo(() => { + const totalRaised = categoryData.reduce((sum, cat) => sum + cat.raised, 0); + if (totalRaised === 0) return []; + + return categoryData.map(cat => ({ + name: cat.activityType.length > 20 ? cat.activityType.substring(0, 20) + '...' : cat.activityType, + value: cat.raised, + fullName: cat.activityType, + percentage: ((cat.raised / totalRaised) * 100).toFixed(0), + })); + }, [categoryData]); + + // Prepare data for bar chart (Status by Category) + const statusByCategoryData = useMemo(() => { + return categoryData.map(cat => ({ + name: cat.activityType.length > 15 ? cat.activityType.substring(0, 15) + '...' : cat.activityType, + fullName: cat.activityType, + Raised: cat.raised, + Approved: cat.approved, + Rejected: cat.rejected, + Pending: cat.pending, + })); + }, [categoryData]); + + // Prepare data for value comparison chart (keep original values, formatCurrency will handle display) + const valueComparisonData = useMemo(() => { + return categoryData.map(cat => ({ + name: cat.activityType.length > 15 ? cat.activityType.substring(0, 15) + '...' : cat.activityType, + fullName: cat.activityType, + Raised: cat.raisedValue, // Keep original value + Approved: cat.approvedValue, // Keep original value + Credited: cat.creditedValue, // Keep original value + })); + }, [categoryData]); + + const COLORS = ['#166534', '#15803d', '#16a34a', '#22c55e', '#4ade80', '#86efac', '#bbf7d0']; + + // Find best performing category + const bestPerforming = useMemo(() => { + if (categoryData.length === 0) return null; + return categoryData.reduce((best, cat) => + cat.approvalRate > (best?.approvalRate || 0) ? cat : best + ); + }, [categoryData]); + + // Find highest value category + const highestValue = useMemo(() => { + if (categoryData.length === 0) return null; + return categoryData.reduce((best, cat) => + cat.raisedValue > (best?.raisedValue || 0) ? cat : best + ); + }, [categoryData]); + + if (loading) { + return ( +
+
+ +

Loading dashboard...

+
+
+ ); + } + + // Show empty state if no data + const hasNoData = kpis.totalClaims === 0 && categoryData.length === 0; + + if (hasNoData) { + return ( +
+ {/* Hero Section */} + +
+ +
+
+
+
+ +
+
+

Claims Analytics Dashboard

+

Comprehensive insights into approval workflows

+
+
+
+ + +
+
+
+
+ + + {/* Empty State */} + + +
+ +
+

No Claims Data Available

+

+ You don't have any claims data yet. Once you create and submit claim requests, your analytics will appear here. +

+
+ + +
+
+
+
+ ); + } + + return ( +
+ {/* Hero Section */} + +
+ +
+
+
+
+ +
+
+

Claims Analytics Dashboard

+

Comprehensive insights into approval workflows

+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+
+ + + {/* KPI Cards */} +
+ + +
+ Raised Claims +
+ +
+
+
+ +
{formatNumber(kpis.totalClaims)}
+

{formatCurrency(kpis.totalValue, true)}

+
+
+ + + +
+ Approved +
+ +
+
+
+ +
{formatNumber(kpis.approved)}
+
+ +

{calculateApprovalRate()}% approval rate

+
+
+
+ + + +
+ Rejected +
+ +
+
+
+ +
{formatNumber(kpis.rejected)}
+
+ +

+ {kpis.totalClaims > 0 ? ((kpis.rejected / kpis.totalClaims) * 100).toFixed(1) : 0}% rejection rate +

+
+
+
+ + + +
+ Pending +
+ +
+
+
+ +
{formatNumber(kpis.pending)}
+

{formatCurrency(kpis.pendingValue)}

+
+
+ + + +
+ Credited +
+ +
+
+
+ +
{formatNumber(kpis.credited)}
+
+ +

{calculateCreditRate()}% credit rate

+
+
+
+ + + +
+ Pending Credit +
+ +
+
+
+ +
{formatNumber(kpis.pendingCredit)}
+

{formatCurrency(kpis.pendingCreditValue)}

+
+
+
+ + {/* Charts Section */} +
+ {/* Distribution by Activity Type */} + + +
+
+ +
+
+ Claims Distribution by Activity Type + Total claims raised across activity types +
+
+
+ + + + `${name}: ${percentage}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {distributionData.map((_entry, index) => ( + + ))} + + + + +
+ {distributionData.slice(0, 3).map((item, index) => ( +
+
+
+

{item.name}

+

{formatNumber(item.value)}

+
+
+ ))} +
+ + + + {/* Status by Category */} + + +
+
+ +
+
+ Claims Status by Activity Type + Count comparison across workflow stages +
+
+
+ + + + + + + + + + + + + + + +
+
+ + {/* Detailed Category Breakdown */} + + +
+
+ +
+
+ Detailed Activity Type Breakdown + In-depth analysis of claims by type and status +
+
+
+ + + + Overview + Top Category 1 + Top Category 2 + + +
+

Activity Type Value Comparison

+ + + + + formatCurrency(value)} /> + formatCurrency(value)} + labelFormatter={(label) => label} + /> + + + + + + +
+
+ {categoryData.slice(0, 3).map((cat, index) => ( + + +
+ {cat.activityType} + + {cat.approvalRate.toFixed(1)}% approved + +
+
+ +
+
+ Raised: + {formatNumber(cat.raised)} ({formatCurrency(cat.raisedValue)}) +
+
+ Approved: + {formatNumber(cat.approved)} ({formatCurrency(cat.approvedValue)}) +
+
+ Rejected: + {formatNumber(cat.rejected)} ({formatCurrency(cat.rejectedValue)}) +
+
+ Pending: + {formatNumber(cat.pending)} ({formatCurrency(cat.pendingValue)}) +
+
+
+ Credited: + {formatNumber(cat.credited)} ({formatCurrency(cat.creditedValue)}) +
+
+ Pending Credit: + {formatNumber(cat.pendingCredit)} ({formatCurrency(cat.pendingCreditValue)}) +
+
+
+
+ Credit Rate + {cat.creditRate.toFixed(1)}% +
+ +
+ + + ))} +
+ + + {/* Category 1 details */} +

Detailed view for top category 1

+
+ + {/* Category 2 details */} +

Detailed view for top category 2

+
+ +
+
+ + {/* Performance Cards */} +
+ + +
+
+ +
+ +
+

Best Performing

+

{bestPerforming?.activityType || 'N/A'}

+

{bestPerforming?.approvalRate.toFixed(2) || 0}% approval rate

+
+
+ + + +
+
+ +
+ +
+

Top Activity Type

+

{highestValue?.activityType || 'N/A'}

+

{highestValue ? formatCurrency(highestValue.raisedValue, true) : '₹0'} raised

+
+
+ + + +
+
+ +
+ +
+

Overall Credit Rate

+

{calculateCreditRate()}%

+

{formatNumber(kpis.credited)} claims credited

+
+
+ + + +
+
+ +
+ +
+

Pending Action

+

{formatNumber(kpis.pendingCredit)}

+

{formatCurrency(kpis.pendingCreditValue)} awaiting credit

+
+
+
+
+ ); +} + diff --git a/src/dealer-claim/pages/RequestDetail.tsx b/src/dealer-claim/pages/RequestDetail.tsx index ad02571..2c4eb65 100644 --- a/src/dealer-claim/pages/RequestDetail.tsx +++ b/src/dealer-claim/pages/RequestDetail.tsx @@ -38,6 +38,8 @@ import { useDocumentUpload } from '@/hooks/useDocumentUpload'; import { useModalManager } from '@/hooks/useModalManager'; import { useConclusionRemark } from '@/hooks/useConclusionRemark'; import { downloadDocument } from '@/services/workflowApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; import { getSocket, joinUserRoom } from '@/utils/socket'; import { isClaimManagementRequest } from '@/utils/claimRequestUtils'; @@ -115,6 +117,24 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam const [showPauseModal, setShowPauseModal] = useState(false); const [showResumeModal, setShowResumeModal] = useState(false); const [showRetriggerModal, setShowRetriggerModal] = useState(false); + const [systemPolicy, setSystemPolicy] = useState<{ + maxApprovalLevels: number; + maxParticipants: number; + allowSpectators: boolean; + maxSpectators: number; + }>({ + maxApprovalLevels: 10, + maxParticipants: 50, + allowSpectators: true, + maxSpectators: 20 + }); + const [policyViolationModal, setPolicyViolationModal] = useState<{ + open: boolean; + violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>; + }>({ + open: false, + violations: [] + }); const { user } = useAuth(); // Custom hooks @@ -251,6 +271,32 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam setShowActionStatusModal ); + // Load system policy on mount + useEffect(() => { + const loadSystemPolicy = async () => { + try { + const systemSettingsConfigs = await getPublicConfigurations('SYSTEM_SETTINGS'); + const workflowSharingConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const allConfigs = [...systemSettingsConfigs, ...workflowSharingConfigs]; + const configMap: Record = {}; + allConfigs.forEach((c: AdminConfiguration) => { + configMap[c.configKey] = c.configValue; + }); + + setSystemPolicy({ + maxApprovalLevels: parseInt(configMap['MAX_APPROVAL_LEVELS'] || '10'), + maxParticipants: parseInt(configMap['MAX_PARTICIPANTS_PER_REQUEST'] || '50'), + allowSpectators: configMap['ALLOW_ADD_SPECTATOR']?.toLowerCase() === 'true', + maxSpectators: parseInt(configMap['MAX_SPECTATORS_PER_REQUEST'] || '20') + }); + } catch (error) { + console.error('Failed to load system policy:', error); + } + }; + + loadSystemPolicy(); + }, []); + // Auto-switch tab when URL query parameter changes useEffect(() => { const urlParams = new URLSearchParams(window.location.search); @@ -639,6 +685,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam isSpectator={isSpectator} currentLevels={currentLevels} onAddApprover={handleAddApprover} + maxApprovalLevels={systemPolicy.maxApprovalLevels} + onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })} />
@@ -728,6 +776,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam actionStatus={actionStatus} existingParticipants={existingParticipants} currentLevels={currentLevels} + maxApprovalLevels={systemPolicy.maxApprovalLevels} + onPolicyViolation={(violations) => setPolicyViolationModal({ open: true, violations })} setShowApproveModal={setShowApproveModal} setShowRejectModal={setShowRejectModal} setShowAddApproverModal={setShowAddApproverModal} @@ -746,6 +796,19 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam downloadDocument={downloadDocument} documentPolicy={documentPolicy} /> + + {/* Policy Violation Modal */} + setPolicyViolationModal({ open: false, violations: [] })} + violations={policyViolationModal.violations} + policyDetails={{ + maxApprovalLevels: systemPolicy.maxApprovalLevels, + maxParticipants: systemPolicy.maxParticipants, + allowSpectators: systemPolicy.allowSpectators, + maxSpectators: systemPolicy.maxSpectators, + }} + /> ); } diff --git a/src/flows.ts b/src/flows.ts index 424d9e6..1b319fb 100644 --- a/src/flows.ts +++ b/src/flows.ts @@ -13,6 +13,7 @@ */ import { RequestFlowType } from '@/utils/requestTypeUtils'; +import { UserFilterType } from '@/utils/userFilterUtils'; // Import flow modules from src/ level import * as CustomFlow from './custom'; @@ -88,6 +89,79 @@ export function getRequestDetailScreen(flowType: RequestFlowType) { } } +/** + * Get Requests Filters component for a user filter type + * Each user type can have its own filter component + * + * This allows for plug-and-play filter components: + * - DEALER: Simplified filters (search + sort only) + * - STANDARD: Full filters (search + status + priority + template + sort) + * + * To add a new user filter type: + * 1. Add the user filter type to UserFilterType in userFilterUtils.ts + * 2. Create a filter component in the appropriate flow folder + * 3. Export it from the flow's index.ts + * 4. Add a case here to return it + */ +export function getRequestsFilters(userFilterType: UserFilterType) { + switch (userFilterType) { + case 'DEALER': + return DealerClaimFlow.DealerRequestsFilters; + case 'STANDARD': + default: + return CustomFlow.StandardRequestsFilters; + } +} + +/** + * Get Closed Requests Filters component for a user filter type + * Each user type can have its own filter component for closed requests + * + * This allows for plug-and-play filter components: + * - DEALER: Simplified filters (search + status + sort only, no priority or template) + * - STANDARD: Full filters (search + priority + status + template + sort) + * + * To add a new user filter type: + * 1. Add the user filter type to UserFilterType in userFilterUtils.ts + * 2. Create a closed requests filter component in the appropriate flow folder + * 3. Export it from the flow's index.ts + * 4. Add a case here to return it + */ +export function getClosedRequestsFilters(userFilterType: UserFilterType) { + switch (userFilterType) { + case 'DEALER': + return DealerClaimFlow.DealerClosedRequestsFilters; + case 'STANDARD': + default: + return CustomFlow.StandardClosedRequestsFilters; + } +} + +/** + * Get User All Requests Filters component for a user filter type + * Each user type can have its own filter component for user all requests + * + * This allows for plug-and-play filter components: + * - DEALER: Simplified filters (search + status + initiator + approver + date range, no priority/template/department/sla) + * - STANDARD: Full filters (all filters including priority, template, department, and SLA compliance) + * + * To add a new user filter type: + * 1. Add the user filter type to UserFilterType in userFilterUtils.ts + * 2. Create a user all requests filter component in the appropriate flow folder + * 3. Export it from the flow's index.ts + * 4. Add a case here to return it + */ +export function getUserAllRequestsFilters(userFilterType: UserFilterType) { + switch (userFilterType) { + case 'DEALER': + return DealerClaimFlow.DealerUserAllRequestsFilters; + case 'STANDARD': + default: + return CustomFlow.StandardUserAllRequestsFilters; + } +} + // Re-export flow modules for direct access export { CustomFlow, DealerClaimFlow, SharedComponents }; export type { RequestFlowType } from '@/utils/requestTypeUtils'; +export type { UserFilterType } from '@/utils/userFilterUtils'; diff --git a/src/hooks/useCreateRequestForm.ts b/src/hooks/useCreateRequestForm.ts index 23514c4..58260cb 100644 --- a/src/hooks/useCreateRequestForm.ts +++ b/src/hooks/useCreateRequestForm.ts @@ -162,9 +162,9 @@ export function useCreateRequestForm( }); // Load system policy - const workflowConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); - const tatConfigs = await getPublicConfigurations('TAT_SETTINGS'); - const allConfigs = [...workflowConfigs, ...tatConfigs]; + const systemSettingsConfigs = await getPublicConfigurations('SYSTEM_SETTINGS'); + const workflowSharingConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const allConfigs = [...systemSettingsConfigs, ...workflowSharingConfigs]; const configMap: Record = {}; allConfigs.forEach((c: AdminConfiguration) => { configMap[c.configKey] = c.configValue; diff --git a/src/pages/ClosedRequests/ClosedRequests.tsx b/src/pages/ClosedRequests/ClosedRequests.tsx index f88bc7d..67f9a00 100644 --- a/src/pages/ClosedRequests/ClosedRequests.tsx +++ b/src/pages/ClosedRequests/ClosedRequests.tsx @@ -1,8 +1,7 @@ -import { useCallback, useRef, useEffect } from 'react'; +import { useCallback, useRef, useEffect, useMemo } from 'react'; // Components import { ClosedRequestsHeader } from './components/ClosedRequestsHeader'; -import { ClosedRequestsFilters as ClosedRequestsFiltersComponent } from './components/ClosedRequestsFilters'; import { ClosedRequestsList } from './components/ClosedRequestsList'; import { ClosedRequestsEmpty } from './components/ClosedRequestsEmpty'; import { ClosedRequestsPagination } from './components/ClosedRequestsPagination'; @@ -14,6 +13,11 @@ import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters'; // Types import type { ClosedRequestsProps } from './types/closedRequests.types'; +// Utils & Factory +import { getUserFilterType } from '@/utils/userFilterUtils'; +import { getClosedRequestsFilters } from '@/flows'; +import { TokenManager } from '@/utils/tokenManager'; + export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { // Data fetching hook const closedRequests = useClosedRequests({ itemsPerPage: 10 }); @@ -23,6 +27,24 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { fetchRef.current = closedRequests.fetchRequests; const filters = useClosedRequestsFilters(); + + // Get user filter type and corresponding filter component (plug-and-play pattern) + const userFilterType = useMemo(() => { + try { + const userData = TokenManager.getUserData(); + return getUserFilterType(userData); + } catch (error) { + console.error('[ClosedRequests] Error getting user filter type:', error); + return 'STANDARD' as const; + } + }, []); + + // Get the appropriate filter component based on user type + const ClosedRequestsFiltersComponent = useMemo(() => { + return getClosedRequestsFilters(userFilterType); + }, [userFilterType]); + + const isDealer = userFilterType === 'DEALER'; const prevFiltersRef = useRef({ searchTerm: filters.searchTerm, statusFilter: filters.statusFilter, @@ -39,14 +61,15 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { fetchRef.current(storedPage, { search: filters.searchTerm || undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, + // Only include priority and templateType filters if user is not a dealer + priority: !isDealer && filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, + templateType: !isDealer && filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, sortBy: filters.sortBy, sortOrder: filters.sortOrder, }); hasInitialFetchRun.current = true; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Only on mount + }, [isDealer]); // Re-fetch if dealer status changes // Track filter changes and refetch useEffect(() => { @@ -88,7 +111,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.sortBy, filters.sortOrder]); + }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.sortBy, filters.sortOrder, isDealer]); // Page change handler const handlePageChange = useCallback( @@ -130,7 +153,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { onRefresh={handleRefresh} /> - {/* Filters */} + {/* Filters - Plug-and-play pattern */} openValidationModal( error.type as 'error' | 'self-assign' | 'not-found', @@ -229,6 +233,7 @@ export function CreateRequest({ error.message ) } + onPolicyViolation={openPolicyViolationModal} /> ); case 4: diff --git a/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts b/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts index 257bce5..a11fe7a 100644 --- a/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts +++ b/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts @@ -9,7 +9,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { RequestTemplate, FormData } from '@/hooks/useCreateRequestForm'; +import { RequestTemplate, FormData, SystemPolicy } from '@/hooks/useCreateRequestForm'; import { PreviewDocument } from '../types/createRequest.types'; import { getDocumentPreviewUrl } from '@/services/workflowApi'; import { validateApprovers } from './useApproverValidation'; @@ -29,6 +29,8 @@ interface UseHandlersOptions { email: string, message: string ) => void; + systemPolicy?: SystemPolicy; + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; onSubmit?: (requestData: any) => void; } @@ -43,6 +45,8 @@ export function useCreateRequestHandlers({ wizardPrevStep, user, openValidationModal, + systemPolicy, + onPolicyViolation, onSubmit, }: UseHandlersOptions) { const navigate = useNavigate(); @@ -93,6 +97,20 @@ export function useCreateRequestHandlers({ // Special validation when leaving step 3 (Approval Workflow) if (currentStep === 3) { + // Validate approval level count against system policy + if (systemPolicy && onPolicyViolation) { + const approverCount = formData.approverCount || 1; + if (approverCount > systemPolicy.maxApprovalLevels) { + onPolicyViolation([{ + type: 'Maximum Approval Levels Exceeded', + message: `The request has ${approverCount} approval levels, which exceeds the maximum allowed (${systemPolicy.maxApprovalLevels}). Please reduce the number of approvers.`, + currentValue: approverCount, + maxValue: systemPolicy.maxApprovalLevels + }]); + return; + } + } + const initiatorEmail = (user as any)?.email?.toLowerCase() || ''; const validation = await validateApprovers( formData.approvers, diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx index 430bc81..3fe502c 100644 --- a/src/pages/OpenRequests/OpenRequests.tsx +++ b/src/pages/OpenRequests/OpenRequests.tsx @@ -1,15 +1,16 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; +import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { useOpenRequestsFilters } from './hooks/useOpenRequestsFilters'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle, Lock } from 'lucide-react'; +import { Calendar, Clock, FileText, AlertCircle, ArrowRight, RefreshCw, CheckCircle, XCircle, Lock, Flame, Target } from 'lucide-react'; import workflowApi from '@/services/workflowApi'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; +import { getUserFilterType } from '@/utils/userFilterUtils'; +import { getRequestsFilters } from '@/flows'; +import { TokenManager } from '@/utils/tokenManager'; interface Request { id: string; title: string; @@ -115,6 +116,40 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { // Use Redux for filters with callback (persists during navigation) const filters = useOpenRequestsFilters(); + // Get user filter type and corresponding filter component (plug-and-play pattern) + const userFilterType = useMemo(() => { + try { + const userData = TokenManager.getUserData(); + return getUserFilterType(userData); + } catch (error) { + console.error('[OpenRequests] Error getting user filter type:', error); + return 'STANDARD' as const; + } + }, []); + + // Get the appropriate filter component based on user type + const RequestsFiltersComponent = useMemo(() => { + return getRequestsFilters(userFilterType); + }, [userFilterType]); + + // Determine once - use this throughout instead of checking repeatedly + const isDealer = userFilterType === 'DEALER'; + + // Helper to build filter params for API - excludes dealer-restricted filters + // Since we know user type initially, this helper uses that knowledge + // Note: This doesn't need useCallback since we'll use it inline in effects to avoid dependency issues + const getFilterParams = (includeStatus?: boolean) => { + return { + search: filters.searchTerm || undefined, + // Only include status, priority, and templateType filters if user is not a dealer + status: includeStatus && !isDealer && filters.statusFilter !== 'all' ? filters.statusFilter : undefined, + priority: !isDealer && filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, + templateType: !isDealer && filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder + }; + }; + // Fetch open requests for the current user only (user-scoped, not organization-wide) // Note: This endpoint returns only requests where the user is: // - An approver (with pending/in-progress status) @@ -192,31 +227,17 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { fetchRequestsRef.current = fetchRequests; - const handleRefresh = () => { + const handleRefresh = useCallback(() => { setRefreshing(true); - fetchRequests(filters.currentPage, { - search: filters.searchTerm || undefined, - status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder - }); - }; + fetchRequests(filters.currentPage, getFilterParams(true)); + }, [filters.currentPage, fetchRequests]); - const handlePageChange = (newPage: number) => { + const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= totalPages) { filters.setCurrentPage(newPage); - fetchRequests(newPage, { - search: filters.searchTerm || undefined, - status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder - }); + fetchRequests(newPage, getFilterParams(true)); } - }; + }, [totalPages, filters, fetchRequests]); const getPageNumbers = () => { const pages = []; @@ -243,14 +264,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { if (!hasInitialFetchRun.current) { hasInitialFetchRun.current = true; const storedPage = filters.currentPage || 1; - fetchRequests(storedPage, { - search: filters.searchTerm || undefined, - status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder, - }); + fetchRequests(storedPage, getFilterParams(true)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only on mount @@ -263,19 +277,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { // Debounce search const timeoutId = setTimeout(() => { filters.setCurrentPage(1); // Reset to page 1 when filters change - fetchRequests(1, { - search: filters.searchTerm || undefined, - status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder, - }); + fetchRequests(1, getFilterParams(true)); }, filters.searchTerm ? 500 : 0); return () => clearTimeout(timeoutId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.sortBy, filters.sortOrder]); + }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.sortBy, filters.sortOrder, isDealer]); // Backend handles both filtering and sorting - use items directly // No client-side sorting needed anymore @@ -316,119 +323,23 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- {/* Enhanced Filters Section */} - - -
-
-
- -
-
- Filters & Search - - {filters.activeFiltersCount > 0 && ( - - {filters.activeFiltersCount} filter{filters.activeFiltersCount > 1 ? 's' : ''} active - - )} - -
-
- {filters.activeFiltersCount > 0 && ( - - )} -
-
- - {/* Primary filters */} -
-
- - filters.setSearchTerm(e.target.value)} - className="pl-9 sm:pl-10 h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white focus:border-blue-400 focus:ring-1 focus:ring-blue-200 transition-colors" - /> -
- - - - - - - -
- - - -
-
-
-
+ {/* Enhanced Filters Section - Plug-and-play pattern */} + {/* Requests List */}
diff --git a/src/pages/RequestDetail/components/RequestDetailModals.tsx b/src/pages/RequestDetail/components/RequestDetailModals.tsx index bc1013d..8a613bf 100644 --- a/src/pages/RequestDetail/components/RequestDetailModals.tsx +++ b/src/pages/RequestDetail/components/RequestDetailModals.tsx @@ -32,6 +32,8 @@ interface RequestDetailModalsProps { actionStatus: any; existingParticipants: any[]; currentLevels: any[]; + maxApprovalLevels?: number; + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; // Handlers setShowApproveModal: (show: boolean) => void; @@ -67,6 +69,8 @@ export function RequestDetailModals({ actionStatus, existingParticipants, currentLevels, + maxApprovalLevels, + onPolicyViolation, setShowApproveModal, setShowRejectModal, setShowAddApproverModal, @@ -114,6 +118,8 @@ export function RequestDetailModals({ requestTitle={request.title} existingParticipants={existingParticipants} currentLevels={currentLevels} + maxApprovalLevels={maxApprovalLevels} + onPolicyViolation={onPolicyViolation} /> {/* Add Spectator Modal */} diff --git a/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx b/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx index da7b537..c9b542e 100644 --- a/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx +++ b/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx @@ -13,6 +13,8 @@ interface WorkNotesTabProps { isSpectator: boolean; currentLevels: any[]; onAddApprover: (email: string, tatHours: number, level: number) => Promise; + maxApprovalLevels?: number; + onPolicyViolation?: (violations: Array<{ type: string; message: string; currentValue?: number; maxValue?: number }>) => void; } export function WorkNotesTab({ @@ -24,6 +26,8 @@ export function WorkNotesTab({ isSpectator, currentLevels, onAddApprover, + maxApprovalLevels, + onPolicyViolation, }: WorkNotesTabProps) { return (
@@ -37,6 +41,8 @@ export function WorkNotesTab({ isSpectator={isSpectator} currentLevels={currentLevels} onAddApprover={onAddApprover} + maxApprovalLevels={maxApprovalLevels} + onPolicyViolation={onPolicyViolation} />
); diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx index 2800a48..3f1cfe0 100644 --- a/src/pages/Requests/UserAllRequests.tsx +++ b/src/pages/Requests/UserAllRequests.tsx @@ -25,6 +25,9 @@ import { useUserSearch } from './hooks/useUserSearch'; // Utils import { transformRequests } from './utils/requestTransformers'; import { exportRequestsToCSV } from './utils/csvExports'; +import { getUserFilterType } from '@/utils/userFilterUtils'; +import { getUserAllRequestsFilters } from '@/flows'; +import { TokenManager } from '@/utils/tokenManager'; // Services import { fetchUserParticipantRequestsData, fetchAllRequestsForExport } from './services/userRequestsService'; @@ -32,22 +35,60 @@ import { fetchUserParticipantRequestsData, fetchAllRequestsForExport } from './s // Types import type { RequestsProps, BackendStats } from './types/requests.types'; -// Filter UI components -import { Card, CardContent } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Badge } from '@/components/ui/badge'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Button } from '@/components/ui/button'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { Label } from '@/components/ui/label'; -import { Separator } from '@/components/ui/separator'; -import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react'; -import { format } from 'date-fns'; - export function UserAllRequests({ onViewRequest }: RequestsProps) { // Filters hook const filters = useRequestsFilters(); + // Get user filter type and corresponding filter component (plug-and-play pattern) + // Determine once at the beginning - no need to check repeatedly + const userFilterType = useMemo(() => { + try { + const userData = TokenManager.getUserData(); + return getUserFilterType(userData); + } catch (error) { + console.error('[UserAllRequests] Error getting user filter type:', error); + return 'STANDARD' as const; + } + }, []); + + // Get the appropriate filter component based on user type + const UserAllRequestsFiltersComponent = useMemo(() => { + return getUserAllRequestsFilters(userFilterType); + }, [userFilterType]); + + // Determine once - use this throughout instead of checking repeatedly + const isDealer = userFilterType === 'DEALER'; + + // Helper to get filters for API - excludes dealer-restricted filters + // Since we know user type initially, this helper uses that knowledge + const getFiltersForApi = useCallback(() => { + const filterOptions = filters.getFilters(); + if (isDealer) { + // For dealers, exclude priority, templateType, department, and slaCompliance + const { priority, templateType, department, slaCompliance, ...dealerFilters } = filterOptions; + return dealerFilters; + } + return filterOptions; + }, [filters, isDealer]); + + // Helper to calculate active filters count based on user type + const calculateActiveFiltersCount = useCallback(() => { + if (isDealer) { + // For dealers: only count search, status, initiator, approver, and date filters + return !!( + filters.searchTerm || + filters.statusFilter !== 'all' || + filters.initiatorFilter !== 'all' || + filters.approverFilter !== 'all' || + filters.dateRange !== 'all' || + filters.customStartDate || + filters.customEndDate + ); + } + // For standard users: count all filters (use existing hasActiveFilters) + return filters.hasActiveFilters; + }, [isDealer, filters]); + // State const [apiRequests, setApiRequests] = useState([]); const [loading, setLoading] = useState(false); @@ -157,12 +198,14 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // Use refs to store stable callbacks to prevent infinite loops const filtersRef = useRef(filters); const fetchBackendStatsRef = useRef(fetchBackendStats); + const getFiltersForApiRef = useRef(getFiltersForApi); // Update refs on each render useEffect(() => { filtersRef.current = filters; fetchBackendStatsRef.current = fetchBackendStats; - }, [filters, fetchBackendStats]); + getFiltersForApiRef.current = getFiltersForApi; + }, [filters, fetchBackendStats, getFiltersForApi]); // Fetch requests - OPTIMIZED: Only fetches 10 records per page const fetchRequests = useCallback(async (page: number = 1) => { @@ -172,7 +215,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { setApiRequests([]); } - const filterOptions = filtersRef.current.getFilters(); + const filterOptions = getFiltersForApiRef.current(); const result = await fetchUserParticipantRequestsData({ page, itemsPerPage, @@ -190,21 +233,22 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { } finally { setLoading(false); } - }, [itemsPerPage]); + }, [itemsPerPage, filters]); // Export to CSV const handleExportToCSV = useCallback(async () => { try { setExporting(true); - const allData = await fetchAllRequestsForExport(filters.getFilters()); - await exportRequestsToCSV(allData, filters.getFilters()); + const exportFilters = getFiltersForApi(); + const allData = await fetchAllRequestsForExport(exportFilters); + await exportRequestsToCSV(allData, exportFilters); } catch (error: any) { console.error('Failed to export requests:', error); alert('Failed to export requests. Please try again.'); } finally { setExporting(false); } - }, [filters]); + }, [getFiltersForApi]); // Initial fetch useEffect(() => { @@ -216,16 +260,30 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { // OPTIMIZED: Uses backend stats API instead of fetching 100 records useEffect(() => { const timeoutId = setTimeout(() => { - const filtersWithoutStatus = { - priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, - templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, - department: filters.departmentFilter !== 'all' ? filters.departmentFilter : undefined, + const filtersWithoutStatus: { + priority?: string; + templateType?: string; + department?: string; + initiator?: string; + approver?: string; + approverType?: 'current' | 'any'; + search?: string; + slaCompliance?: string; + } = { initiator: filters.initiatorFilter !== 'all' ? filters.initiatorFilter : undefined, approver: filters.approverFilter !== 'all' ? filters.approverFilter : undefined, approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined, search: filters.searchTerm || undefined, - slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined }; + + // Only include priority, templateType, department, and slaCompliance if user is not a dealer + if (!isDealer) { + if (filters.priorityFilter !== 'all') filtersWithoutStatus.priority = filters.priorityFilter; + if (filters.templateTypeFilter !== 'all') filtersWithoutStatus.templateType = filters.templateTypeFilter; + if (filters.departmentFilter !== 'all') filtersWithoutStatus.department = filters.departmentFilter; + if (filters.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter; + } + // Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month' const statsDateRange = filters.dateRange === 'all' ? 'all' : (filters.dateRange || 'month'); @@ -250,7 +308,8 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { filters.dateRange, filters.customStartDate, filters.customEndDate, - filters.templateTypeFilter + filters.templateTypeFilter, + isDealer // Note: statusFilter is NOT in dependencies - stats don't change when only status changes ]); @@ -415,342 +474,42 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { }} /> - {/* Filters */} - - -
-
-
- -

Advanced Filters

- {filters.hasActiveFilters && ( - - Active - - )} -
- {filters.hasActiveFilters && ( - - )} -
- - - - {/* Primary Filters */} -
-
- - filters.setSearchTerm(e.target.value)} - className="pl-10 h-10" - data-testid="search-input" - /> -
- - - - - - - - - - -
- - {/* User Filters - Initiator and Approver */} -
- {/* Initiator Filter */} -
- -
- {initiatorSearch.selectedUser ? ( -
- - {initiatorSearch.selectedUser.displayName || initiatorSearch.selectedUser.email} - - -
- ) : ( - <> - initiatorSearch.handleSearch(e.target.value)} - onFocus={() => { - if (initiatorSearch.searchResults.length > 0) { - initiatorSearch.setShowResults(true); - } - }} - onBlur={() => setTimeout(() => initiatorSearch.setShowResults(false), 200)} - className="h-10" - data-testid="initiator-search-input" - /> - {initiatorSearch.showResults && initiatorSearch.searchResults.length > 0 && ( -
- {initiatorSearch.searchResults.map((user) => ( - - ))} -
- )} - - )} -
-
- - {/* Approver Filter */} -
-
- - {filters.approverFilter !== 'all' && ( - - )} -
-
- {approverSearch.selectedUser ? ( -
- - {approverSearch.selectedUser.displayName || approverSearch.selectedUser.email} - - -
- ) : ( - <> - approverSearch.handleSearch(e.target.value)} - onFocus={() => { - if (approverSearch.searchResults.length > 0) { - approverSearch.setShowResults(true); - } - }} - onBlur={() => setTimeout(() => approverSearch.setShowResults(false), 200)} - className="h-10" - data-testid="approver-search-input" - /> - {approverSearch.showResults && approverSearch.searchResults.length > 0 && ( -
- {approverSearch.searchResults.map((user) => ( - - ))} -
- )} - - )} -
-
-
- - {/* Date Range Filter */} -
- - - - {filters.dateRange === 'custom' && ( - - - - - -
-
-
- - { - const date = e.target.value ? new Date(e.target.value) : undefined; - if (date) { - filters.setCustomStartDate(date); - if (filters.customEndDate && date > filters.customEndDate) { - filters.setCustomEndDate(date); - } - } else { - filters.setCustomStartDate(undefined); - } - }} - max={format(new Date(), 'yyyy-MM-dd')} - className="w-full" - /> -
-
- - { - const date = e.target.value ? new Date(e.target.value) : undefined; - if (date) { - filters.setCustomEndDate(date); - if (filters.customStartDate && date < filters.customStartDate) { - filters.setCustomStartDate(date); - } - } else { - filters.setCustomEndDate(undefined); - } - }} - min={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : undefined} - max={format(new Date(), 'yyyy-MM-dd')} - className="w-full" - /> -
-
-
- - -
-
-
-
- )} -
-
-
-
+ {/* Filters - Plug-and-play pattern */} + {/* Requests List */} { } } +export interface DashboardKPIs { + totalClaims: number; + totalValue: number; + approved: number; + rejected: number; + pending: number; + credited: number; + pendingCredit: number; + approvedValue: number; + rejectedValue: number; + pendingValue: number; + creditedValue: number; + pendingCreditValue: number; +} + +export interface CategoryData { + activityType: string; + raised: number; + raisedValue: number; + approved: number; + approvedValue: number; + rejected: number; + rejectedValue: number; + pending: number; + pendingValue: number; + credited: number; + creditedValue: number; + pendingCredit: number; + pendingCreditValue: number; + approvalRate: number; + creditRate: number; +} + +export interface DealerDashboardData { + kpis: DashboardKPIs; + categoryData: CategoryData[]; +} + +/** + * Get dealer dashboard KPIs and category data + * GET /api/v1/dealer-claims/dashboard + */ +export async function getDealerDashboard( + dateRange?: string, + startDate?: string, + endDate?: string +): Promise { + try { + const params: any = {}; + if (dateRange) params.dateRange = dateRange; + if (startDate) params.startDate = startDate; + if (endDate) params.endDate = endDate; + + const response = await apiClient.get('/dealer-claims/dashboard', { params }); + return response.data?.data || response.data; + } catch (error: any) { + console.error('[DealerClaimAPI] Error fetching dealer dashboard:', error); + throw error; + } +} + diff --git a/src/utils/userFilterUtils.ts b/src/utils/userFilterUtils.ts new file mode 100644 index 0000000..fe339e7 --- /dev/null +++ b/src/utils/userFilterUtils.ts @@ -0,0 +1,34 @@ +/** + * User Filter Type Detection Utilities + * + * Determines which filter component to use based on user role/job title. + * This allows for plug-and-play filter components per user type. + */ + +export type UserFilterType = 'DEALER' | 'STANDARD'; + +/** + * Check if user is a dealer based on job title + */ +export function isDealerUser(user: any): boolean { + if (!user) return false; + + // Check job title + if (user.jobTitle === 'Dealer' || user.jobTitle === 'DEALER') { + return true; + } + + return false; +} + +/** + * Get the filter type for a user + * Returns the appropriate UserFilterType based on user properties + */ +export function getUserFilterType(user: any): UserFilterType { + if (isDealerUser(user)) { + return 'DEALER'; + } + return 'STANDARD'; +} +