dealer claimm related code commented

This commit is contained in:
laxmanhalaki 2026-01-23 20:36:20 +05:30
parent 6c5398f433
commit efdcb18b64
10 changed files with 216 additions and 216 deletions

View File

@ -77,7 +77,7 @@ export function StandardClosedRequestsFilters({
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6"> <CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Input <Input
@ -88,7 +88,7 @@ export function StandardClosedRequestsFilters({
data-testid="closed-requests-search" data-testid="closed-requests-search"
/> />
</div> </div>
<Select value={priorityFilter} onValueChange={onPriorityChange}> <Select value={priorityFilter} onValueChange={onPriorityChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-priority-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-priority-filter">
<SelectValue placeholder="All Priorities" /> <SelectValue placeholder="All Priorities" />
@ -109,7 +109,7 @@ export function StandardClosedRequestsFilters({
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={statusFilter} onValueChange={onStatusChange}> <Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-status-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-status-filter">
<SelectValue placeholder="Closure Type" /> <SelectValue placeholder="Closure Type" />
@ -130,7 +130,7 @@ export function StandardClosedRequestsFilters({
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{/*
<Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}> <Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-template-type-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-template-type-filter">
<SelectValue placeholder="All Templates" /> <SelectValue placeholder="All Templates" />
@ -140,7 +140,7 @@ export function StandardClosedRequestsFilters({
<SelectItem value="CUSTOM">Non-Templatized</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={sortBy} onValueChange={(value) => onSortByChange(value as 'created' | 'due' | 'priority')}> <Select value={sortBy} onValueChange={(value) => onSortByChange(value as 'created' | 'due' | 'priority')}>
@ -153,7 +153,7 @@ export function StandardClosedRequestsFilters({
<SelectItem value="priority">Priority</SelectItem> <SelectItem value="priority">Priority</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@ -77,7 +77,7 @@ export function StandardRequestsFilters({
</CardHeader> </CardHeader>
<CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6"> <CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6">
{/* Standard filters - Search, Status, Priority, Template Type, and Sort */} {/* Standard filters - Search, Status, Priority, Template Type, and Sort */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Input <Input
@ -87,7 +87,7 @@ export function StandardRequestsFilters({
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" 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"
/> />
</div> </div>
<Select value={priorityFilter} onValueChange={onPriorityFilterChange}> <Select value={priorityFilter} onValueChange={onPriorityFilterChange}>
<SelectTrigger className="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"> <SelectTrigger className="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">
<SelectValue placeholder="All Priorities" /> <SelectValue placeholder="All Priorities" />
@ -108,7 +108,7 @@ export function StandardRequestsFilters({
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={statusFilter} onValueChange={onStatusFilterChange}> <Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger className="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"> <SelectTrigger className="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">
<SelectValue placeholder="All Statuses" /> <SelectValue placeholder="All Statuses" />
@ -120,7 +120,7 @@ export function StandardRequestsFilters({
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={templateTypeFilter} onValueChange={onTemplateTypeFilterChange}> {/* <Select value={templateTypeFilter} onValueChange={onTemplateTypeFilterChange}>
<SelectTrigger className="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"> <SelectTrigger className="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">
<SelectValue placeholder="All Templates" /> <SelectValue placeholder="All Templates" />
</SelectTrigger> </SelectTrigger>
@ -129,7 +129,7 @@ export function StandardRequestsFilters({
<SelectItem value="CUSTOM">Non-Templatized</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={sortBy} onValueChange={(value: any) => onSortByChange(value)}> <Select value={sortBy} onValueChange={(value: any) => onSortByChange(value)}>
@ -143,7 +143,7 @@ export function StandardRequestsFilters({
<SelectItem value="sla">SLA Progress</SelectItem> <SelectItem value="sla">SLA Progress</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@ -34,11 +34,11 @@ interface StandardUserAllRequestsFiltersProps {
customStartDate?: Date; customStartDate?: Date;
customEndDate?: Date; customEndDate?: Date;
showCustomDatePicker: boolean; showCustomDatePicker: boolean;
// Departments // Departments
departments: string[]; departments: string[];
loadingDepartments: boolean; loadingDepartments: boolean;
// State for user search // State for user search
initiatorSearch: { initiatorSearch: {
selectedUser: { userId: string; email: string; displayName?: string } | null; selectedUser: { userId: string; email: string; displayName?: string } | null;
@ -50,7 +50,7 @@ interface StandardUserAllRequestsFiltersProps {
handleClear: () => void; handleClear: () => void;
setShowResults: (show: boolean) => void; setShowResults: (show: boolean) => void;
}; };
approverSearch: { approverSearch: {
selectedUser: { userId: string; email: string; displayName?: string } | null; selectedUser: { userId: string; email: string; displayName?: string } | null;
searchQuery: string; searchQuery: string;
@ -61,7 +61,7 @@ interface StandardUserAllRequestsFiltersProps {
handleClear: () => void; handleClear: () => void;
setShowResults: (show: boolean) => void; setShowResults: (show: boolean) => void;
}; };
// Actions // Actions
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onStatusChange: (value: string) => void; onStatusChange: (value: string) => void;
@ -78,7 +78,7 @@ interface StandardUserAllRequestsFiltersProps {
onShowCustomDatePickerChange?: (show: boolean) => void; onShowCustomDatePickerChange?: (show: boolean) => void;
onApplyCustomDate?: () => void; onApplyCustomDate?: () => void;
onClearFilters: () => void; onClearFilters: () => void;
// Computed // Computed
hasActiveFilters: boolean; hasActiveFilters: boolean;
} }
@ -143,7 +143,7 @@ export function StandardUserAllRequestsFilters({
<Separator /> <Separator />
{/* Primary Filters */} {/* Primary Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div className="relative md:col-span-3 lg:col-span-1"> <div className="relative md:col-span-3 lg:col-span-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input <Input
@ -180,7 +180,7 @@ export function StandardUserAllRequestsFilters({
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}> {/* <Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}>
<SelectTrigger className="h-10" data-testid="template-type-filter"> <SelectTrigger className="h-10" data-testid="template-type-filter">
<SelectValue placeholder="All Templates" /> <SelectValue placeholder="All Templates" />
</SelectTrigger> </SelectTrigger>
@ -189,7 +189,7 @@ export function StandardUserAllRequestsFilters({
<SelectItem value="CUSTOM">Custom</SelectItem> <SelectItem value="CUSTOM">Custom</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
<Select <Select
value={departmentFilter} value={departmentFilter}

View File

@ -28,7 +28,7 @@ export function Auth() {
// Clear any existing session data // Clear any existing session data
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
try { try {
await login(); await login();
} catch (loginError) { } catch (loginError) {
@ -45,7 +45,7 @@ export function Auth() {
// Clear any existing session data // Clear any existing session data
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
setTanflowLoading(true); setTanflowLoading(true);
try { try {
initiateTanflowLogin(); initiateTanflowLogin();
@ -65,7 +65,7 @@ export function Auth() {
} }
return ( return (
<div <div
className="min-h-screen flex items-center justify-center p-4 relative" className="min-h-screen flex items-center justify-center p-4 relative"
style={{ style={{
backgroundImage: imageLoaded ? `url(${LandingPageImage})` : 'none', backgroundImage: imageLoaded ? `url(${LandingPageImage})` : 'none',
@ -81,19 +81,19 @@ export function Auth() {
)} )}
{/* Overlay for better readability */} {/* Overlay for better readability */}
<div className="absolute inset-0 bg-black/40"></div> <div className="absolute inset-0 bg-black/40"></div>
<Card className="w-full max-w-md shadow-xl relative z-10 bg-black backdrop-blur-sm border-gray-800"> <Card className="w-full max-w-md shadow-xl relative z-10 bg-black backdrop-blur-sm border-gray-800">
<CardHeader className="space-y-1 text-center pb-6"> <CardHeader className="space-y-1 text-center pb-6">
<div className="flex flex-col items-center justify-center mb-4"> <div className="flex flex-col items-center justify-center mb-4">
<img <img
src={ReLogo} src={ReLogo}
alt="Royal Enfield Logo" alt="Royal Enfield Logo"
className="h-10 w-auto max-w-[168px] object-contain mb-2" className="h-10 w-auto max-w-[168px] object-contain mb-2"
/> />
<p className="text-xs text-gray-300 text-center truncate">Approval Portal</p> <p className="text-xs text-gray-300 text-center truncate">Approval Portal</p>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{error && ( {error && (
<div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 rounded-lg"> <div className="bg-red-900/50 border border-red-700 text-red-200 px-4 py-3 rounded-lg">
@ -101,7 +101,7 @@ export function Auth() {
<p className="text-sm">{error.message}</p> <p className="text-sm">{error.message}</p>
</div> </div>
)} )}
<div className="space-y-3"> <div className="space-y-3">
<Button <Button
onClick={handleOKTALogin} onClick={handleOKTALogin}
@ -111,8 +111,8 @@ export function Auth() {
> >
{isLoading ? ( {isLoading ? (
<> <>
<div <div
className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
/> />
Logging in... Logging in...
</> </>
@ -123,7 +123,7 @@ export function Auth() {
</> </>
)} )}
</Button> </Button>
{/*
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-gray-700"></span> <span className="w-full border-t border-gray-700"></span>
@ -152,7 +152,7 @@ export function Auth() {
Dealer Login Dealer Login
</> </>
)} )}
</Button> </Button> */}
</div> </div>
<div className="text-center text-sm text-gray-400 mt-4"> <div className="text-center text-sm text-gray-400 mt-4">

View File

@ -75,7 +75,7 @@ export function ClosedRequestsFilters({
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6"> <CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-3.5 h-3.5 sm:w-4 sm:h-4" />
<Input <Input
@ -86,7 +86,7 @@ export function ClosedRequestsFilters({
data-testid="closed-requests-search" data-testid="closed-requests-search"
/> />
</div> </div>
<Select value={priorityFilter} onValueChange={onPriorityChange}> <Select value={priorityFilter} onValueChange={onPriorityChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-priority-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-priority-filter">
<SelectValue placeholder="All Priorities" /> <SelectValue placeholder="All Priorities" />
@ -107,7 +107,7 @@ export function ClosedRequestsFilters({
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={statusFilter} onValueChange={onStatusChange}> <Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-status-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-status-filter">
<SelectValue placeholder="Closure Type" /> <SelectValue placeholder="Closure Type" />
@ -129,7 +129,7 @@ export function ClosedRequestsFilters({
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}> {/* <Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}>
<SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-template-type-filter"> <SelectTrigger className="h-9 sm:h-10 md:h-11 text-sm sm:text-base bg-gray-50 border-gray-200 focus:bg-white" data-testid="closed-requests-template-type-filter">
<SelectValue placeholder="All Templates" /> <SelectValue placeholder="All Templates" />
</SelectTrigger> </SelectTrigger>
@ -138,7 +138,7 @@ export function ClosedRequestsFilters({
<SelectItem value="CUSTOM">Non-Templatized</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
<div className="flex gap-2"> <div className="flex gap-2">
<Select value={sortBy} onValueChange={(value) => onSortByChange(value as 'created' | 'due' | 'priority')}> <Select value={sortBy} onValueChange={(value) => onSortByChange(value as 'created' | 'due' | 'priority')}>
@ -151,7 +151,7 @@ export function ClosedRequestsFilters({
<SelectItem value="priority">Priority</SelectItem> <SelectItem value="priority">Priority</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@ -76,7 +76,7 @@ export function MyRequestsFilters({
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}> {/* <Select value={templateTypeFilter} onValueChange={onTemplateTypeChange}>
<SelectTrigger <SelectTrigger
className="flex-1 md:w-28 lg:w-32 text-xs sm:text-sm bg-white border-gray-300 hover:border-gray-400 focus:border-blue-400 focus:ring-1 focus:ring-blue-200 h-9 sm:h-10" className="flex-1 md:w-28 lg:w-32 text-xs sm:text-sm bg-white border-gray-300 hover:border-gray-400 focus:border-blue-400 focus:ring-1 focus:ring-blue-200 h-9 sm:h-10"
data-testid="template-type-filter" data-testid="template-type-filter"
@ -88,7 +88,7 @@ export function MyRequestsFilters({
<SelectItem value="CUSTOM">Non-Templatized</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
</div> </div>
</div> </div>
</CardContent> </CardContent>

View File

@ -18,9 +18,9 @@ interface Request {
status: 'pending' | 'approved' | 'rejected' | 'closed' | 'paused'; status: 'pending' | 'approved' | 'rejected' | 'closed' | 'paused';
priority: 'express' | 'standard'; priority: 'express' | 'standard';
initiator: { name: string; avatar: string }; initiator: { name: string; avatar: string };
currentApprover?: { currentApprover?: {
name: string; name: string;
avatar: string; avatar: string;
sla?: any; // Backend-calculated SLA data sla?: any; // Backend-calculated SLA data
}; };
createdAt: string; createdAt: string;
@ -106,13 +106,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [items, setItems] = useState<Request[]>([]); const [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// Pagination states (currentPage now in Redux) // Pagination states (currentPage now in Redux)
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalRecords, setTotalRecords] = useState(0); const [totalRecords, setTotalRecords] = useState(0);
const [itemsPerPage] = useState(10); const [itemsPerPage] = useState(10);
const fetchRequestsRef = useRef<any>(null); const fetchRequestsRef = useRef<any>(null);
// Use Redux for filters with callback (persists during navigation) // Use Redux for filters with callback (persists during navigation)
const filters = useOpenRequestsFilters(); const filters = useOpenRequestsFilters();
@ -163,12 +163,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
setLoading(true); setLoading(true);
setItems([]); setItems([]);
} }
// Always use user-scoped endpoint (not organization-wide) // Always use user-scoped endpoint (not organization-wide)
// Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user) // Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user)
// For organization-wide requests, use the "All Requests" screen (/requests) // For organization-wide requests, use the "All Requests" screen (/requests)
const result = await workflowApi.listOpenForMe({ const result = await workflowApi.listOpenForMe({
page, page,
limit: itemsPerPage, limit: itemsPerPage,
search: filterParams?.search, search: filterParams?.search,
status: filterParams?.status, status: filterParams?.status,
@ -177,12 +177,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
sortBy: filterParams?.sortBy, sortBy: filterParams?.sortBy,
sortOrder: filterParams?.sortOrder sortOrder: filterParams?.sortOrder
}); });
// Extract data - workflowApi now returns { data: [], pagination: {} } // Extract data - workflowApi now returns { data: [], pagination: {} }
const data = Array.isArray((result as any)?.data) const data = Array.isArray((result as any)?.data)
? (result as any).data ? (result as any).data
: []; : [];
// Set pagination data // Set pagination data
const pagination = (result as any)?.pagination; const pagination = (result as any)?.pagination;
if (pagination) { if (pagination) {
@ -190,10 +190,10 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
setTotalPages(pagination.totalPages || 1); setTotalPages(pagination.totalPages || 1);
setTotalRecords(pagination.total || 0); setTotalRecords(pagination.total || 0);
} }
const mapped: Request[] = data.map((r: any) => { const mapped: Request[] = data.map((r: any) => {
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at; const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
return { return {
id: r.requestNumber || r.request_number || r.requestId, id: r.requestNumber || r.request_number || r.requestId,
requestId: r.requestId, requestId: r.requestId,
@ -202,13 +202,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
description: r.description, description: r.description,
status: (r.status || '').toString().toLowerCase().replace('_', '-'), status: (r.status || '').toString().toLowerCase().replace('_', '-'),
priority: (r.priority || '').toString().toLowerCase(), priority: (r.priority || '').toString().toLowerCase(),
initiator: { initiator: {
name: (r.initiator?.displayName || r.initiator?.email || '—'), name: (r.initiator?.displayName || r.initiator?.email || '—'),
avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0, 2).toUpperCase())
}, },
currentApprover: r.currentApprover ? { currentApprover: r.currentApprover ? {
name: (r.currentApprover.name || r.currentApprover.email || '—'), name: (r.currentApprover.name || r.currentApprover.email || '—'),
avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()), avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0, 2).toUpperCase()),
sla: r.currentApprover.sla // ← Backend-calculated SLA sla: r.currentApprover.sla // ← Backend-calculated SLA
} : undefined, } : undefined,
createdAt: createdAt || '—', createdAt: createdAt || '—',
@ -224,7 +224,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
setRefreshing(false); setRefreshing(false);
} }
}, [itemsPerPage, filters]); }, [itemsPerPage, filters]);
fetchRequestsRef.current = fetchRequests; fetchRequestsRef.current = fetchRequests;
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
@ -244,21 +244,21 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const maxPagesToShow = 5; const maxPagesToShow = 5;
let startPage = Math.max(1, filters.currentPage - Math.floor(maxPagesToShow / 2)); let startPage = Math.max(1, filters.currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage < maxPagesToShow - 1) { if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1); startPage = Math.max(1, endPage - maxPagesToShow + 1);
} }
for (let i = startPage; i <= endPage; i++) { for (let i = startPage; i <= endPage; i++) {
pages.push(i); pages.push(i);
} }
return pages; return pages;
}; };
// Track if this is initial mount // Track if this is initial mount
const hasInitialFetchRun = useRef(false); const hasInitialFetchRun = useRef(false);
// Initial fetch on mount - use stored page from Redux // Initial fetch on mount - use stored page from Redux
useEffect(() => { useEffect(() => {
if (!hasInitialFetchRun.current) { if (!hasInitialFetchRun.current) {
@ -268,7 +268,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only on mount }, []); // Only on mount
// Track previous filter values to detect changes // Track previous filter values to detect changes
const prevFiltersRef = useRef({ const prevFiltersRef = useRef({
searchTerm: filters.searchTerm, searchTerm: filters.searchTerm,
@ -284,11 +284,11 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
useEffect(() => { useEffect(() => {
// Skip until initial fetch has completed // Skip until initial fetch has completed
if (!hasInitialFetchRun.current) return; if (!hasInitialFetchRun.current) return;
const prev = prevFiltersRef.current; const prev = prevFiltersRef.current;
// Check if any filter actually changed // Check if any filter actually changed
const hasChanged = const hasChanged =
prev.searchTerm !== filters.searchTerm || prev.searchTerm !== filters.searchTerm ||
prev.statusFilter !== filters.statusFilter || prev.statusFilter !== filters.statusFilter ||
prev.priorityFilter !== filters.priorityFilter || prev.priorityFilter !== filters.priorityFilter ||
@ -303,7 +303,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
filters.setCurrentPage(1); // Reset to page 1 when filters change (but not on initial mount or back navigation) filters.setCurrentPage(1); // Reset to page 1 when filters change (but not on initial mount or back navigation)
fetchRequests(1, getFilterParams(true)); fetchRequests(1, getFilterParams(true));
// Update previous filters ref // Update previous filters ref
prevFiltersRef.current = { prevFiltersRef.current = {
searchTerm: filters.searchTerm, searchTerm: filters.searchTerm,
@ -325,7 +325,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const filteredAndSortedRequests = items; const filteredAndSortedRequests = items;
return ( return (
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto"> <div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
{/* Enhanced Header */} {/* Enhanced Header */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4 md:gap-6"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4 md:gap-6">
@ -340,15 +340,15 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 sm:gap-3"> <div className="flex items-center gap-2 sm:gap-3">
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold"> <Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
{loading ? 'Loading…' : `${totalRecords || items.length} open`} {loading ? 'Loading…' : `${totalRecords || items.length} open`}
<span className="hidden sm:inline ml-1">requests</span> <span className="hidden sm:inline ml-1">requests</span>
</Badge> </Badge>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="gap-1 sm:gap-2 h-8 sm:h-9" className="gap-1 sm:gap-2 h-8 sm:h-9"
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
@ -382,10 +382,10 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{filteredAndSortedRequests.map((request) => { {filteredAndSortedRequests.map((request) => {
const priorityConfig = getPriorityConfig(request.priority); const priorityConfig = getPriorityConfig(request.priority);
const statusConfig = getStatusConfig(request.status); const statusConfig = getStatusConfig(request.status);
return ( return (
<Card <Card
key={request.id} key={request.id}
className="group hover:shadow-lg transition-all duration-200 cursor-pointer border border-gray-200 hover:border-blue-400 hover:scale-[1.002]" className="group hover:shadow-lg transition-all duration-200 cursor-pointer border border-gray-200 hover:border-blue-400 hover:scale-[1.002]"
onClick={() => onViewRequest?.(request.id, request.title)} onClick={() => onViewRequest?.(request.id, request.title)}
> >
@ -405,8 +405,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<h3 className="text-base font-bold text-gray-900 group-hover:text-blue-600 transition-colors"> <h3 className="text-base font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
{(request as any).displayId || request.id} {(request as any).displayId || request.id}
</h3> </h3>
<Badge <Badge
variant="outline" variant="outline"
className={`${statusConfig.color} text-xs px-2.5 py-0.5 font-semibold shrink-0`} className={`${statusConfig.color} text-xs px-2.5 py-0.5 font-semibold shrink-0`}
> >
<statusConfig.icon className="w-3.5 h-3.5 mr-1" /> <statusConfig.icon className="w-3.5 h-3.5 mr-1" />
@ -417,8 +417,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{request.department} {request.department}
</Badge> </Badge>
)} )}
<Badge <Badge
variant="outline" variant="outline"
className={`${priorityConfig.color} text-xs px-2.5 py-0.5 capitalize hidden md:inline-flex`} className={`${priorityConfig.color} text-xs px-2.5 py-0.5 capitalize hidden md:inline-flex`}
> >
{request.priority} {request.priority}
@ -427,18 +427,18 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{(() => { {(() => {
const templateType = (request as any)?.templateType || (request as any)?.template_type || ''; const templateType = (request as any)?.templateType || (request as any)?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || ''; const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType // Direct mapping from templateType
let templateLabel = 'Non-Templatized'; let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200'; let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') { if (templateTypeUpper === 'DEALER CLAIM') {
templateLabel = 'Dealer Claim'; templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200'; templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') { } else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template'; templateLabel = 'Template';
} }
return ( return (
<Badge <Badge
variant="outline" variant="outline"
@ -460,15 +460,15 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{request.currentLevelSLA && (() => { {request.currentLevelSLA && (() => {
// Check pause status from isPaused field, pauseInfo, OR status field // Check pause status from isPaused field, pauseInfo, OR status field
const isPaused = Boolean( const isPaused = Boolean(
request.isPaused || request.isPaused ||
request.pauseInfo?.isPaused || request.pauseInfo?.isPaused ||
request.status === 'paused' request.status === 'paused'
); );
// Use percentage-based colors to match approver SLA tracker // Use percentage-based colors to match approver SLA tracker
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
// Grey: When paused (frozen state) // Grey: When paused (frozen state)
const percentUsed = request.currentLevelSLA.percentageUsed || 0; const percentUsed = request.currentLevelSLA.percentageUsed || 0;
const getSLAColors = () => { const getSLAColors = () => {
// If paused, always use grey colors (frozen state) // If paused, always use grey colors (frozen state)
if (isPaused) { if (isPaused) {
@ -479,7 +479,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
icon: 'text-gray-600' icon: 'text-gray-600'
}; };
} }
if (percentUsed >= 100) { if (percentUsed >= 100) {
return { return {
bg: 'bg-red-50 border border-red-200', bg: 'bg-red-50 border border-red-200',
@ -510,9 +510,9 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
}; };
} }
}; };
const colors = getSLAColors(); const colors = getSLAColors();
return ( return (
<div className={`p-2 rounded-md ${colors.bg}`}> <div className={`p-2 rounded-md ${colors.bg}`}>
<div className="flex items-center justify-between mb-1.5"> <div className="flex items-center justify-between mb-1.5">
@ -533,8 +533,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</span> </span>
</div> </div>
</div> </div>
<Progress <Progress
value={percentUsed} value={percentUsed}
className="h-1.5" className="h-1.5"
indicatorClassName={colors.progress} indicatorClassName={colors.progress}
/> />
@ -552,7 +552,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</Avatar> </Avatar>
<span className="font-medium text-gray-900">{request.initiator.name}</span> <span className="font-medium text-gray-900">{request.initiator.name}</span>
</div> </div>
{request.currentApprover && ( {request.currentApprover && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Avatar className="h-6 w-6 ring-2 ring-yellow-200 shadow-sm"> <Avatar className="h-6 w-6 ring-2 ring-yellow-200 shadow-sm">
@ -563,14 +563,14 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<span className="font-medium text-gray-900">{request.currentApprover.name}</span> <span className="font-medium text-gray-900">{request.currentApprover.name}</span>
</div> </div>
)} )}
{request.approvalStep && ( {request.approvalStep && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 text-blue-500" /> <AlertCircle className="w-3.5 h-3.5 text-blue-500" />
<span className="font-medium">{request.approvalStep}</span> <span className="font-medium">{request.approvalStep}</span>
</div> </div>
)} )}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" /> <Calendar className="w-3.5 h-3.5" />
<span>Created: {request.createdAt !== '—' ? formatDateDDMMYYYY(request.createdAt) : '—'}</span> <span>Created: {request.createdAt !== '—' ? formatDateDDMMYYYY(request.createdAt) : '—'}</span>
@ -604,8 +604,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
} }
</p> </p>
{filters.activeFiltersCount > 0 && ( {filters.activeFiltersCount > 0 && (
<Button <Button
variant="outline" variant="outline"
className="mt-4" className="mt-4"
onClick={filters.clearFilters} onClick={filters.clearFilters}
> >
@ -624,7 +624,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<div className="text-xs sm:text-sm text-muted-foreground"> <div className="text-xs sm:text-sm text-muted-foreground">
Showing {((filters.currentPage - 1) * itemsPerPage) + 1} to {Math.min(filters.currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests Showing {((filters.currentPage - 1) * itemsPerPage) + 1} to {Math.min(filters.currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="outline" variant="outline"
@ -635,14 +635,14 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
> >
<ArrowRight className="h-4 w-4 rotate-180" /> <ArrowRight className="h-4 w-4 rotate-180" />
</Button> </Button>
{filters.currentPage > 3 && totalPages > 5 && ( {filters.currentPage > 3 && totalPages > 5 && (
<> <>
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button> <Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
<span className="text-muted-foreground">...</span> <span className="text-muted-foreground">...</span>
</> </>
)} )}
{getPageNumbers().map((pageNum) => ( {getPageNumbers().map((pageNum) => (
<Button <Button
key={pageNum} key={pageNum}
@ -654,14 +654,14 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
{pageNum} {pageNum}
</Button> </Button>
))} ))}
{filters.currentPage < totalPages - 2 && totalPages > 5 && ( {filters.currentPage < totalPages - 2 && totalPages > 5 && (
<> <>
<span className="text-muted-foreground">...</span> <span className="text-muted-foreground">...</span>
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button> <Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button>
</> </>
)} )}
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@ -49,10 +49,10 @@ import { CustomDatePicker } from '@/components/ui/date-picker';
export function Requests({ onViewRequest }: RequestsProps) { export function Requests({ onViewRequest }: RequestsProps) {
const { user } = useAuth(); const { user } = useAuth();
// Get viewAsUser from Redux store (synced with Dashboard toggle) // Get viewAsUser from Redux store (synced with Dashboard toggle)
const viewAsUser = useAppSelector((state) => state.dashboard.viewAsUser); const viewAsUser = useAppSelector((state) => state.dashboard.viewAsUser);
// Determine if viewing at organization level: // Determine if viewing at organization level:
// - If user is admin/management AND not in "Personal" mode (viewAsUser=false) → show all requests // - If user is admin/management AND not in "Personal" mode (viewAsUser=false) → show all requests
// - If user is admin/management AND in "Personal" mode (viewAsUser=true) → show only their requests // - If user is admin/management AND in "Personal" mode (viewAsUser=true) → show only their requests
@ -95,15 +95,15 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Status filter should not affect stats - stats should always show all status counts // Status filter should not affect stats - stats should always show all status counts
// For user-level (Personal mode), stats will only include requests where user is involved // For user-level (Personal mode), stats will only include requests where user is involved
const fetchBackendStats = useCallback(async ( const fetchBackendStats = useCallback(async (
statsDateRange?: DateRange, statsDateRange?: DateRange,
statsStartDate?: Date, statsStartDate?: Date,
statsEndDate?: Date, statsEndDate?: Date,
filtersWithoutStatus?: { filtersWithoutStatus?: {
priority?: string; priority?: string;
department?: string; department?: string;
initiator?: string; initiator?: string;
approver?: string; approver?: string;
approverType?: 'current' | 'any'; approverType?: 'current' | 'any';
search?: string; search?: string;
slaCompliance?: string; slaCompliance?: string;
} }
@ -112,9 +112,9 @@ export function Requests({ onViewRequest }: RequestsProps) {
// For dynamic SLA statuses (on_track, approaching, critical), we need to fetch and enrich // For dynamic SLA statuses (on_track, approaching, critical), we need to fetch and enrich
// because these are calculated dynamically, not stored in DB // because these are calculated dynamically, not stored in DB
const slaCompliance = filtersWithoutStatus?.slaCompliance; const slaCompliance = filtersWithoutStatus?.slaCompliance;
const isDynamicSlaStatus = slaCompliance && const isDynamicSlaStatus = slaCompliance &&
slaCompliance !== 'all' && slaCompliance !== 'all' &&
slaCompliance !== 'breached' && slaCompliance !== 'breached' &&
slaCompliance !== 'compliant'; slaCompliance !== 'compliant';
if (isDynamicSlaStatus) { if (isDynamicSlaStatus) {
@ -141,12 +141,12 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Fetch up to 1000 requests (backend will enrich and filter by SLA) // Fetch up to 1000 requests (backend will enrich and filter by SLA)
// Use appropriate API based on org/personal mode // Use appropriate API based on org/personal mode
const result = isOrgLevel const result = isOrgLevel
? await workflowApi.listWorkflows({ page: 1, limit: 1000, ...backendFilters }) ? await workflowApi.listWorkflows({ page: 1, limit: 1000, ...backendFilters })
: await workflowApi.listParticipantRequests({ page: 1, limit: 1000, ...backendFilters }); : await workflowApi.listParticipantRequests({ page: 1, limit: 1000, ...backendFilters });
const filteredData = Array.isArray(result?.data) ? result.data : []; const filteredData = Array.isArray(result?.data) ? result.data : [];
// Calculate stats from filtered data // Calculate stats from filtered data
const total = filteredData.length; const total = filteredData.length;
const pending = filteredData.filter((r: any) => { const pending = filteredData.filter((r: any) => {
@ -244,7 +244,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Use refs to store stable callbacks to prevent infinite loops // Use refs to store stable callbacks to prevent infinite loops
const filtersRef = useRef(filters); const filtersRef = useRef(filters);
const fetchBackendStatsRef = useRef(fetchBackendStats); const fetchBackendStatsRef = useRef(fetchBackendStats);
// Update refs on each render // Update refs on each render
useEffect(() => { useEffect(() => {
filtersRef.current = filters; filtersRef.current = filters;
@ -254,7 +254,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Parse URL search params // Parse URL search params
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
// Approver Filter (from Dashboard TAT Breach Report link) // Approver Filter (from Dashboard TAT Breach Report link)
const approver = params.get('approver'); const approver = params.get('approver');
const approverType = params.get('approverType'); const approverType = params.get('approverType');
@ -341,35 +341,35 @@ export function Requests({ onViewRequest }: RequestsProps) {
// Total changes when other filters are applied, but stays stable when only status changes // Total changes when other filters are applied, but stays stable when only status changes
// Stats are fetched for both org-level AND user-level (Personal mode) views // Stats are fetched for both org-level AND user-level (Personal mode) views
useEffect(() => { useEffect(() => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
const filtersWithoutStatus = { const filtersWithoutStatus = {
priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined, priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined, templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined,
department: filters.departmentFilter !== 'all' ? filters.departmentFilter : undefined, department: filters.departmentFilter !== 'all' ? filters.departmentFilter : undefined,
initiator: filters.initiatorFilter !== 'all' ? filters.initiatorFilter : undefined, initiator: filters.initiatorFilter !== 'all' ? filters.initiatorFilter : undefined,
approver: filters.approverFilter !== 'all' ? filters.approverFilter : undefined, approver: filters.approverFilter !== 'all' ? filters.approverFilter : undefined,
approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined, approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined,
search: filters.searchTerm || undefined, search: filters.searchTerm || undefined,
slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined
}; };
// All Requests (admin/normal user) should always have a date range // All Requests (admin/normal user) should always have a date range
// Default to 'all' if no date range is selected // Default to 'all' if no date range is selected
const statsDateRange = filters.dateRange || 'all'; const statsDateRange = filters.dateRange || 'all';
fetchBackendStatsRef.current(
statsDateRange,
filters.customStartDate,
filters.customEndDate,
filtersWithoutStatus
);
}, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId); fetchBackendStatsRef.current(
statsDateRange,
filters.customStartDate,
filters.customEndDate,
filtersWithoutStatus
);
}, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
isOrgLevel, isOrgLevel,
filters.dateRange, filters.dateRange,
filters.customStartDate, filters.customStartDate,
filters.customEndDate, filters.customEndDate,
filters.priorityFilter, filters.priorityFilter,
filters.templateTypeFilter, filters.templateTypeFilter,
@ -399,7 +399,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
isOrgLevel, isOrgLevel,
}); });
const hasInitialFetchRun = useRef(false); const hasInitialFetchRun = useRef(false);
// Initial fetch on mount - use stored page from Redux // Initial fetch on mount - use stored page from Redux
useEffect(() => { useEffect(() => {
const storedPage = filters.currentPage || 1; const storedPage = filters.currentPage || 1;
@ -407,13 +407,13 @@ export function Requests({ onViewRequest }: RequestsProps) {
hasInitialFetchRun.current = true; hasInitialFetchRun.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only on mount }, []); // Only on mount
// Fetch when filters change or isOrgLevel changes // Fetch when filters change or isOrgLevel changes
useEffect(() => { useEffect(() => {
if (!hasInitialFetchRun.current) return; if (!hasInitialFetchRun.current) return;
const prev = prevFiltersRef.current; const prev = prevFiltersRef.current;
const hasChanged = const hasChanged =
prev.searchTerm !== filters.searchTerm || prev.searchTerm !== filters.searchTerm ||
prev.statusFilter !== filters.statusFilter || prev.statusFilter !== filters.statusFilter ||
prev.priorityFilter !== filters.priorityFilter || prev.priorityFilter !== filters.priorityFilter ||
@ -427,13 +427,13 @@ export function Requests({ onViewRequest }: RequestsProps) {
prev.customStartDate !== filters.customStartDate || prev.customStartDate !== filters.customStartDate ||
prev.customEndDate !== filters.customEndDate || prev.customEndDate !== filters.customEndDate ||
prev.isOrgLevel !== isOrgLevel; prev.isOrgLevel !== isOrgLevel;
if (!hasChanged) return; if (!hasChanged) return;
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
filters.setCurrentPage(1); filters.setCurrentPage(1);
fetchRequests(1); fetchRequests(1);
prevFiltersRef.current = { prevFiltersRef.current = {
searchTerm: filters.searchTerm, searchTerm: filters.searchTerm,
statusFilter: filters.statusFilter, statusFilter: filters.statusFilter,
@ -495,7 +495,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
closed: backendStats.closed || 0 closed: backendStats.closed || 0
}; };
} }
// Fallback: Calculate from paginated data (less accurate, but better than nothing) // Fallback: Calculate from paginated data (less accurate, but better than nothing)
return calculateStatsFromFilteredData( return calculateStatsFromFilteredData(
[], // Empty - we'll use backendStats or fallback [], // Empty - we'll use backendStats or fallback
@ -521,8 +521,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
/> />
{/* Stats */} {/* Stats */}
<RequestsStats <RequestsStats
stats={stats} stats={stats}
onStatusFilter={(status) => { onStatusFilter={(status) => {
filters.setStatusFilter(status); filters.setStatusFilter(status);
}} }}
@ -553,7 +553,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
<Separator /> <Separator />
{/* Primary Filters */} {/* Primary Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div className="relative md:col-span-3 lg:col-span-1"> <div className="relative md:col-span-3 lg:col-span-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input <Input
@ -590,7 +590,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={filters.templateTypeFilter} onValueChange={filters.setTemplateTypeFilter}> {/* <Select value={filters.templateTypeFilter} onValueChange={filters.setTemplateTypeFilter}>
<SelectTrigger className="h-10" data-testid="template-type-filter"> <SelectTrigger className="h-10" data-testid="template-type-filter">
<SelectValue placeholder="All Templates" /> <SelectValue placeholder="All Templates" />
</SelectTrigger> </SelectTrigger>
@ -599,7 +599,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
<SelectItem value="CUSTOM">Non-Templatized</SelectItem> <SelectItem value="CUSTOM">Non-Templatized</SelectItem>
<SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem> <SelectItem value="DEALER CLAIM">Dealer Claim</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select> */}
<Select <Select
value={filters.departmentFilter} value={filters.departmentFilter}
@ -792,23 +792,23 @@ export function Requests({ onViewRequest }: RequestsProps) {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="start-date">Start Date</Label> <Label htmlFor="start-date">Start Date</Label>
<CustomDatePicker <CustomDatePicker
value={filters.customStartDate || null} value={filters.customStartDate || null}
onChange={(dateStr: string | null) => { onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined; const date = dateStr ? new Date(dateStr) : undefined;
if (date) { if (date) {
filters.setCustomStartDate(date); filters.setCustomStartDate(date);
if (filters.customEndDate && date > filters.customEndDate) { if (filters.customEndDate && date > filters.customEndDate) {
filters.setCustomEndDate(date); filters.setCustomEndDate(date);
}
} else {
filters.setCustomStartDate(undefined);
} }
}} } else {
maxDate={new Date()} filters.setCustomStartDate(undefined);
placeholderText="dd/mm/yyyy" }
className="w-full" }}
/> maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="end-date">End Date</Label> <Label htmlFor="end-date">End Date</Label>

View File

@ -15,23 +15,23 @@ import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
*/ */
const stripHtmlTags = (html: string): string => { const stripHtmlTags = (html: string): string => {
if (!html) return ''; if (!html) return '';
// Check if we're in a browser environment // Check if we're in a browser environment
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
// Fallback for SSR: use regex to strip HTML tags // Fallback for SSR: use regex to strip HTML tags
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
} }
// Create a temporary div to parse HTML // Create a temporary div to parse HTML
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = html; tempDiv.innerHTML = html;
// Get text content (automatically strips HTML tags) // Get text content (automatically strips HTML tags)
let text = tempDiv.textContent || tempDiv.innerText || ''; let text = tempDiv.textContent || tempDiv.innerText || '';
// Clean up extra whitespace // Clean up extra whitespace
text = text.replace(/\s+/g, ' ').trim(); text = text.replace(/\s+/g, ' ').trim();
return text; return text;
}; };
@ -100,18 +100,18 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
{(() => { {(() => {
const templateType = request?.templateType || (request as any)?.template_type || ''; const templateType = request?.templateType || (request as any)?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || ''; const templateTypeUpper = templateType?.toUpperCase() || '';
// Direct mapping from templateType // Direct mapping from templateType
let templateLabel = 'Non-Templatized'; let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200'; let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
if (templateTypeUpper === 'DEALER CLAIM') { if (templateTypeUpper === 'DEALER CLAIM') {
templateLabel = 'Dealer Claim'; templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200'; templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') { } else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template'; templateLabel = 'Template';
} }
return ( return (
<Badge <Badge
variant="outline" variant="outline"

View File

@ -1,9 +1,9 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { import {
Settings as SettingsIcon, Settings as SettingsIcon,
Bell, Bell,
Shield, Shield,
Palette, Palette,
Lock, Lock,
@ -54,7 +54,7 @@ export function Settings() {
const handleEnableNotifications = async () => { const handleEnableNotifications = async () => {
setIsEnablingNotifications(true); setIsEnablingNotifications(true);
setShowNotificationModal(false); setShowNotificationModal(false);
try { try {
// Check if notifications are supported // Check if notifications are supported
if (!('Notification' in window)) { if (!('Notification' in window)) {
@ -67,7 +67,7 @@ export function Settings() {
// Check current permission status BEFORE attempting to enable // Check current permission status BEFORE attempting to enable
let permission = Notification.permission; let permission = Notification.permission;
// If permission was previously denied, show user-friendly instructions // If permission was previously denied, show user-friendly instructions
if (permission === 'denied') { if (permission === 'denied') {
setNotificationSuccess(false); setNotificationSuccess(false);
@ -87,7 +87,7 @@ export function Settings() {
// If permission is 'default', request it first // If permission is 'default', request it first
if (permission === 'default') { if (permission === 'default') {
permission = await Notification.requestPermission(); permission = await Notification.requestPermission();
// If user denied the permission request // If user denied the permission request
if (permission === 'denied') { if (permission === 'denied') {
setNotificationSuccess(false); setNotificationSuccess(false);
@ -126,13 +126,13 @@ export function Settings() {
// Provide more specific error messages // Provide more specific error messages
const errorMessage = error?.message || 'Unknown error occurred'; const errorMessage = error?.message || 'Unknown error occurred';
setNotificationMessage( setNotificationMessage(
errorMessage.includes('permission') errorMessage.includes('permission')
? 'Notification permission was denied. Please enable notifications in your browser settings and try again.' ? 'Notification permission was denied. Please enable notifications in your browser settings and try again.'
: errorMessage.includes('Service worker') : errorMessage.includes('Service worker')
? 'Service worker registration failed. Please refresh the page and try again.' ? 'Service worker registration failed. Please refresh the page and try again.'
: errorMessage.includes('token') : errorMessage.includes('token')
? 'Authentication required. Please log in again and try enabling notifications.' ? 'Authentication required. Please log in again and try enabling notifications.'
: `Unable to enable push notifications: ${errorMessage}` : `Unable to enable push notifications: ${errorMessage}`
); );
setShowNotificationModal(true); setShowNotificationModal(true);
} finally { } finally {
@ -147,7 +147,7 @@ export function Settings() {
<Card className="relative overflow-hidden shadow-2xl border-0 rounded-2xl"> <Card className="relative overflow-hidden shadow-2xl border-0 rounded-2xl">
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"></div> <div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"></div>
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-yellow-400/20 via-transparent to-transparent"></div> <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-yellow-400/20 via-transparent to-transparent"></div>
<CardContent className="relative z-10 p-6 sm:p-8 lg:p-12"> <CardContent className="relative z-10 p-6 sm:p-8 lg:p-12">
<div className="flex items-center gap-4 sm:gap-6"> <div className="flex items-center gap-4 sm:gap-6">
<div className="w-14 h-14 sm:w-16 sm:h-16 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-2xl flex items-center justify-center shadow-xl transform hover:scale-105 transition-transform"> <div className="w-14 h-14 sm:w-16 sm:h-16 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-2xl flex items-center justify-center shadow-xl transform hover:scale-105 transition-transform">
@ -164,47 +164,47 @@ export function Settings() {
{/* Tabs for Admin, Cards for Non-Admin */} {/* Tabs for Admin, Cards for Non-Admin */}
{isAdmin ? ( {isAdmin ? (
<Tabs defaultValue="user" className="w-full"> <Tabs defaultValue="user" className="w-full">
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-5 mb-8 bg-slate-100 p-1 rounded-xl h-auto gap-1"> <TabsList className="grid w-full grid-cols-2 lg:grid-cols-4 mb-8 bg-slate-100 p-1 rounded-xl h-auto gap-1">
<TabsTrigger <TabsTrigger
value="user" value="user"
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all" className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
> >
<SettingsIcon className="w-4 h-4" /> <SettingsIcon className="w-4 h-4" />
<span className="hidden sm:inline">User Settings</span> <span className="hidden sm:inline">User Settings</span>
<span className="sm:hidden">User</span> <span className="sm:hidden">User</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="roles" value="roles"
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all" className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
> >
<Shield className="w-4 h-4" /> <Shield className="w-4 h-4" />
<span className="hidden sm:inline">User Roles</span> <span className="hidden sm:inline">User Roles</span>
<span className="sm:hidden">Roles</span> <span className="sm:hidden">Roles</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="system" value="system"
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all" className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
> >
<Sliders className="w-4 h-4" /> <Sliders className="w-4 h-4" />
<span className="hidden sm:inline">Configuration</span> <span className="hidden sm:inline">Configuration</span>
<span className="sm:hidden">Config</span> <span className="sm:hidden">Config</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="holidays" value="holidays"
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all" className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
> >
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
<span className="hidden sm:inline">Holidays</span> <span className="hidden sm:inline">Holidays</span>
<span className="sm:hidden">Holidays</span> <span className="sm:hidden">Holidays</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger {/* <TabsTrigger
value="templates" value="templates"
className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all" className="flex items-center justify-center gap-2 py-3 rounded-lg data-[state=active]:bg-white data-[state=active]:shadow-md transition-all"
> >
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
<span className="hidden sm:inline">Templates</span> <span className="hidden sm:inline">Templates</span>
<span className="sm:hidden">Templates</span> <span className="sm:hidden">Templates</span>
</TabsTrigger> </TabsTrigger> */}
</TabsList> </TabsList>
{/* Fixed width container to prevent layout shifts */} {/* Fixed width container to prevent layout shifts */}
@ -240,12 +240,12 @@ export function Settings() {
) : ( ) : (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md"> <div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-xs text-blue-800"> <p className="text-xs text-blue-800">
Click below to register this browser for receiving push notifications. Click below to register this browser for receiving push notifications.
This needs to be done once per browser/device. This needs to be done once per browser/device.
</p> </p>
</div> </div>
)} )}
<Button <Button
onClick={handleEnableNotifications} onClick={handleEnableNotifications}
disabled={isEnablingNotifications || hasSubscription} disabled={isEnablingNotifications || hasSubscription}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
@ -315,7 +315,7 @@ export function Settings() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button <Button
onClick={() => setShowPreferencesModal(true)} onClick={() => setShowPreferencesModal(true)}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
> >
@ -362,7 +362,7 @@ export function Settings() {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
{/* Dealer Claim Activity Settings Card */} {/* Dealer Claim Activity Settings Card */}
<Card <Card
className="shadow-md hover:shadow-lg transition-all duration-300 border border-slate-200 rounded-lg cursor-pointer group" className="shadow-md hover:shadow-lg transition-all duration-300 border border-slate-200 rounded-lg cursor-pointer group"
onClick={() => setShowActivityTypeManager(true)} onClick={() => setShowActivityTypeManager(true)}
> >
@ -460,12 +460,12 @@ export function Settings() {
) : ( ) : (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md"> <div className="p-3 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-xs text-blue-800"> <p className="text-xs text-blue-800">
Click below to register this browser for receiving push notifications. Click below to register this browser for receiving push notifications.
This needs to be done once per browser/device. This needs to be done once per browser/device.
</p> </p>
</div> </div>
)} )}
<Button <Button
onClick={handleEnableNotifications} onClick={handleEnableNotifications}
disabled={isEnablingNotifications || hasSubscription} disabled={isEnablingNotifications || hasSubscription}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
@ -535,7 +535,7 @@ export function Settings() {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Button <Button
onClick={() => setShowPreferencesModal(true)} onClick={() => setShowPreferencesModal(true)}
className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all" className="w-full bg-re-green hover:bg-re-green/90 text-white shadow-md hover:shadow-lg transition-all"
> >
@ -548,14 +548,14 @@ export function Settings() {
</> </>
)} )}
</div> </div>
<NotificationStatusModal <NotificationStatusModal
open={showNotificationModal} open={showNotificationModal}
onClose={() => setShowNotificationModal(false)} onClose={() => setShowNotificationModal(false)}
success={notificationSuccess} success={notificationSuccess}
message={notificationMessage} message={notificationMessage}
/> />
<NotificationPreferencesModal <NotificationPreferencesModal
open={showPreferencesModal} open={showPreferencesModal}
onClose={() => setShowPreferencesModal(false)} onClose={() => setShowPreferencesModal(false)}