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 (
+
+ );
+ }
+
+ // 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 && (
+
+
+ Clear
+
+ )}
+
+
+
+
+
+
+ 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"
+ />
+
+
+
+
+
+
+
+ All Priorities
+
+
+
+ Express
+
+
+
+
+
+ Standard
+
+
+
+
+
+
+
+
+
+
+ All Closures
+
+
+
+ Closed After Approval
+
+
+
+
+
+ Closed After Rejection
+
+
+
+
+
+
+
+
+
+
+ All Templates
+ Custom
+ Dealer Claim
+
+
+
+
+ onSortByChange(value as 'created' | 'due' | 'priority')}>
+
+
+
+
+ Due Date
+ Date Created
+ Priority
+
+
+
+
+ {sortOrder === 'asc' ? : }
+
+
+
+
+
+ );
+}
+
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 && (
+
+
+ Clear
+
+ )}
+
+
+
+ {/* 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"
+ />
+
+
+
+
+
+
+
+ All Priorities
+
+
+
+ Express
+
+
+
+
+
+ Standard
+
+
+
+
+
+
+
+
+
+
+ All Statuses
+ Pending (In Approval)
+ Approved (Needs Closure)
+
+
+
+
+
+
+
+
+ All Templates
+ Custom
+ Dealer Claim
+
+
+
+
+ onSortByChange(value)}>
+
+
+
+
+ Due Date
+ Date Created
+ Priority
+ SLA Progress
+
+
+
+ onSortOrderChange(sortOrder === 'asc' ? 'desc' : 'asc')}
+ className="px-2 sm:px-3 h-9 sm:h-10 md:h-11"
+ >
+ {sortOrder === 'asc' ? : }
+
+
+
+
+
+ );
+}
+
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 && (
+
+
+ Clear All
+
+ )}
+
+
+
+
+ {/* Primary Filters */}
+
+
+
+ onSearchChange(e.target.value)}
+ className="pl-10 h-10"
+ data-testid="search-input"
+ />
+
+
+
+
+
+
+
+ All Status
+ Pending
+ Paused
+ Approved
+ Rejected
+ Closed
+
+
+
+
+
+
+
+
+ All Priority
+ Express
+ Standard
+
+
+
+
+
+
+
+
+ All Templates
+ Custom
+ Dealer Claim
+
+
+
+
+
+
+
+
+ All Departments
+ {departments.map((dept) => (
+ {dept}
+ ))}
+
+
+
+
+
+
+
+
+ All SLA Status
+ Compliant
+ On Track
+ Approaching
+ Critical
+ Breached
+
+
+
+
+ {/* User Filters - Initiator and Approver */}
+
+ {/* Initiator Filter */}
+
+
Initiator
+
+ {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) => (
+
initiatorSearch.handleSelect(user)}
+ className="w-full px-4 py-2 text-left hover:bg-gray-50"
+ >
+
+
+ {user.displayName || user.email}
+
+ {user.displayName && (
+ {user.email}
+ )}
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+ {/* Approver Filter */}
+
+
+ Approver
+ {approverFilter !== 'all' && onApproverTypeChange && (
+ onApproverTypeChange(value)}
+ >
+
+
+
+
+ Current Only
+ Any Approver
+
+
+ )}
+
+
+ {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) => (
+
approverSearch.handleSelect(user)}
+ className="w-full px-4 py-2 text-left hover:bg-gray-50"
+ >
+
+
+ {user.displayName || user.email}
+
+ {user.displayName && (
+ {user.email}
+ )}
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+
+ {/* Date Range Filter */}
+
+
+
onDateRangeChange(value as DateRange)}>
+
+
+
+
+ All Time
+ Today
+ This Week
+ This Month
+ Last 7 Days
+ Last 30 Days
+ Custom Range
+
+
+
+ {dateRange === 'custom' && (
+
+
+
+
+ {customStartDate && customEndDate
+ ? `${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`
+ : 'Select dates'}
+
+
+
+
+
+
+
+ Apply
+
+ {
+ onShowCustomDatePickerChange?.(false);
+ onCustomStartDateChange?.(undefined);
+ onCustomEndDateChange?.(undefined);
+ onDateRangeChange('month');
+ }}
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
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 && (
+
+
+ Clear
+
+ )}
+
+
+
+ {/* 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 && (
+
+
+
+
+
+ All Closures
+
+
+
+ Closed After Approval
+
+
+
+
+
+ Closed After Rejection
+
+
+
+
+ )}
+
+
+ onSortByChange(value as 'created' | 'due' | 'priority')}>
+
+
+
+
+ Due Date
+ Date Created
+ Priority
+
+
+
+
+ {sortOrder === 'asc' ? : }
+
+
+
+
+
+ );
+}
+
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 && (
+
+
+ Clear
+
+ )}
+
+
+
+ {/* 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"
+ />
+
+
+
+ onSortByChange(value)}>
+
+
+
+
+ Due Date
+ Date Created
+ Priority
+ SLA Progress
+
+
+
+ onSortOrderChange(sortOrder === 'asc' ? 'desc' : 'asc')}
+ className="px-2 sm:px-3 h-9 sm:h-10 md:h-11"
+ >
+ {sortOrder === 'asc' ? : }
+
+
+
+
+
+ );
+}
+
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 && (
+
+
+ Clear All
+
+ )}
+
+
+
+
+ {/* Primary Filters - Only Search and Status for dealers */}
+
+
+
+ onSearchChange(e.target.value)}
+ className="pl-10 h-10"
+ data-testid="dealer-search-input"
+ />
+
+
+
+
+
+
+
+ All Status
+ Pending
+ Paused
+ Approved
+ Rejected
+ Closed
+
+
+
+
+ {/* User Filters - Initiator and Approver */}
+
+ {/* Initiator Filter */}
+
+
Initiator
+
+ {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) => (
+
initiatorSearch.handleSelect(user)}
+ className="w-full px-4 py-2 text-left hover:bg-gray-50"
+ >
+
+
+ {user.displayName || user.email}
+
+ {user.displayName && (
+ {user.email}
+ )}
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+ {/* Approver Filter */}
+
+
+ Approver
+ {approverFilter !== 'all' && onApproverTypeChange && (
+ onApproverTypeChange(value)}
+ >
+
+
+
+
+ Current Only
+ Any Approver
+
+
+ )}
+
+
+ {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) => (
+
approverSearch.handleSelect(user)}
+ className="w-full px-4 py-2 text-left hover:bg-gray-50"
+ >
+
+
+ {user.displayName || user.email}
+
+ {user.displayName && (
+ {user.email}
+ )}
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+
+
+
+ {/* Date Range Filter */}
+
+
+
onDateRangeChange(value as DateRange)}>
+
+
+
+
+ All Time
+ Today
+ This Week
+ This Month
+ Last 7 Days
+ Last 30 Days
+ Custom Range
+
+
+
+ {dateRange === 'custom' && (
+
+
+
+
+ {customStartDate && customEndDate
+ ? `${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`
+ : 'Select dates'}
+
+
+
+
+
+
+
+ Apply
+
+ {
+ onShowCustomDatePickerChange?.(false);
+ onCustomStartDateChange?.(undefined);
+ onCustomEndDateChange?.(undefined);
+ onDateRangeChange('month');
+ }}
+ >
+ Cancel
+
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
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;
+ })()}
+
+ )}
⚠️ Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final.
+
+ {/* Max Approval Levels Note */}
+ {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
+
+
+
+ onNavigate?.('/new-request')}
+ className="bg-blue-600 hover:bg-blue-700 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-200"
+ >
+
+ Create New Claim
+
+ {
+ setRefreshing(true);
+ fetchDashboardData(true);
+ }}
+ disabled={refreshing}
+ variant="outline"
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20"
+ >
+
+ Refresh
+
+
+
+
+
+
+
+ {/* 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.
+
+
+ onNavigate?.('/new-request')}
+ className="bg-blue-600 hover:bg-blue-700 text-white"
+ >
+
+ Create Your First Claim
+
+ {
+ setRefreshing(true);
+ fetchDashboardData(true);
+ }}
+ disabled={refreshing}
+ variant="outline"
+ >
+
+ Refresh Data
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Hero Section */}
+
+
+
+
+
+
+
+
+
+
+
Claims Analytics Dashboard
+
Comprehensive insights into approval workflows
+
+
+
+ onNavigate?.('/requests?status=pending')}
+ className="bg-blue-600 hover:bg-blue-700 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-200"
+ >
+
+ View Pending Claims
+
+ onNavigate?.('/requests')}
+ className="bg-emerald-600 hover:bg-emerald-700 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-200"
+ >
+
+ My Claims
+
+
+
+
+
+
+
+
+ {/* KPI Cards */}
+
+
+
+
+
+
+ {formatNumber(kpis.totalClaims)}
+ {formatCurrency(kpis.totalValue, true)}
+
+
+
+
+
+
+
+
+ {formatNumber(kpis.approved)}
+
+
+
{calculateApprovalRate()}% approval rate
+
+
+
+
+
+
+
+
+
+ {formatNumber(kpis.rejected)}
+
+
+
+ {kpis.totalClaims > 0 ? ((kpis.rejected / kpis.totalClaims) * 100).toFixed(1) : 0}% rejection rate
+
+
+
+
+
+
+
+
+
+
+ {formatNumber(kpis.pending)}
+ {formatCurrency(kpis.pendingValue)}
+
+
+
+
+
+
+
+
+ {formatNumber(kpis.credited)}
+
+
+
{calculateCreditRate()}% credit rate
+
+
+
+
+
+
+
+
+
+ {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 && (
-
-
- Clear
-
- )}
-
-
-
- {/* 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"
- />
-
-
-
-
-
-
-
- All Priorities
-
-
-
- Express
-
-
-
-
-
- Standard
-
-
-
-
-
-
-
-
-
-
- All Statuses
- Pending (In Approval)
- Approved (Needs Closure)
-
-
-
-
-
-
-
-
- All Templates
- Custom
- Dealer Claim
-
-
-
-
- filters.setSortBy(value)}>
-
-
-
-
- Due Date
- Date Created
- Priority
- SLA Progress
-
-
-
- filters.setSortOrder(filters.sortOrder === 'asc' ? 'desc' : 'asc')}
- className="px-2 sm:px-3 h-9 sm:h-10 md:h-11"
- >
- {filters.sortOrder === 'asc' ? : }
-
-
-
-
-
+ {/* 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 && (
-
-
- Clear All
-
- )}
-
-
-
-
- {/* Primary Filters */}
-
-
-
- filters.setSearchTerm(e.target.value)}
- className="pl-10 h-10"
- data-testid="search-input"
- />
-
-
-
-
-
-
-
- All Status
- Pending
- Paused
- Approved
- Rejected
- Closed
-
-
-
-
-
-
-
-
- All Priority
- Express
- Standard
-
-
-
-
-
-
-
-
- All Templates
- Custom
- Dealer Claim
-
-
-
-
-
-
-
-
- All Departments
- {departments.map((dept) => (
- {dept}
- ))}
-
-
-
-
-
-
-
-
- All SLA Status
- Compliant
- On Track
- Approaching
- Critical
- Breached
-
-
-
-
- {/* User Filters - Initiator and Approver */}
-
- {/* Initiator Filter */}
-
-
Initiator
-
- {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) => (
-
initiatorSearch.handleSelect(user)}
- className="w-full px-4 py-2 text-left hover:bg-gray-50"
- >
-
-
- {user.displayName || user.email}
-
- {user.displayName && (
- {user.email}
- )}
-
-
- ))}
-
- )}
- >
- )}
-
-
-
- {/* Approver Filter */}
-
-
- Approver
- {filters.approverFilter !== 'all' && (
- filters.setApproverFilterType(value)}
- >
-
-
-
-
- Current Only
- Any Approver
-
-
- )}
-
-
- {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) => (
-
approverSearch.handleSelect(user)}
- className="w-full px-4 py-2 text-left hover:bg-gray-50"
- >
-
-
- {user.displayName || user.email}
-
- {user.displayName && (
- {user.email}
- )}
-
-
- ))}
-
- )}
- >
- )}
-
-
-
-
- {/* Date Range Filter */}
-
-
-
-
-
-
-
- All Time
- Today
- This Week
- This Month
- Last 7 Days
- Last 30 Days
- Custom Range
-
-
-
- {filters.dateRange === 'custom' && (
-
-
-
-
- {filters.customStartDate && filters.customEndDate
- ? `${format(filters.customStartDate, 'MMM d, yyyy')} - ${format(filters.customEndDate, 'MMM d, yyyy')}`
- : 'Select dates'}
-
-
-
-
-
-
-
- Apply
-
- {
- filters.setShowCustomDatePicker(false);
- filters.setCustomStartDate(undefined);
- filters.setCustomEndDate(undefined);
- filters.setDateRange('month');
- }}
- >
- Cancel
-
-
-
-
-
- )}
-
-
-
-
+ {/* 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';
+}
+