dealer claimm related code commented
This commit is contained in:
parent
6c5398f433
commit
efdcb18b64
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user