-
diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx
index 1d283c2..0a8f1d7 100644
--- a/src/pages/MyRequests/MyRequests.tsx
+++ b/src/pages/MyRequests/MyRequests.tsx
@@ -24,7 +24,7 @@ interface MyRequestsProps {
export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsProps) {
const { user } = useAuth();
-
+
// Data fetching hook
const myRequests = useMyRequests({ itemsPerPage: 10 });
@@ -38,9 +38,10 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
statusFilter: filters.statusFilter,
priorityFilter: filters.priorityFilter,
templateTypeFilter: filters.templateTypeFilter,
+ lifecycleFilter: filters.lifecycleFilter,
});
const hasInitialFetchRun = useRef(false);
-
+
// Initial fetch on mount - use stored page from Redux
useEffect(() => {
const storedPage = filters.currentPage || 1;
@@ -49,46 +50,50 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined,
+ lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
});
hasInitialFetchRun.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only on mount
-
+
// Track filter changes and refetch
useEffect(() => {
if (!hasInitialFetchRun.current) return;
-
+
const prev = prevFiltersRef.current;
- const hasChanged =
+ const hasChanged =
prev.searchTerm !== filters.searchTerm ||
prev.statusFilter !== filters.statusFilter ||
prev.priorityFilter !== filters.priorityFilter ||
- prev.templateTypeFilter !== filters.templateTypeFilter;
-
+ prev.templateTypeFilter !== filters.templateTypeFilter ||
+ prev.lifecycleFilter !== filters.lifecycleFilter;
+
if (!hasChanged) return; // No actual change, skip
-
+
// Debounce search
const timeoutId = setTimeout(() => {
filters.setCurrentPage(1); // Reset to page 1 when filters change
- fetchRef.current(1, {
+ fetchRef.current(1, {
search: filters.searchTerm || undefined,
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined,
- });
-
+ lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
+ });
+
// Update previous values
prevFiltersRef.current = {
searchTerm: filters.searchTerm,
statusFilter: filters.statusFilter,
priorityFilter: filters.priorityFilter,
templateTypeFilter: filters.templateTypeFilter,
+ lifecycleFilter: filters.lifecycleFilter,
};
}, filters.searchTerm !== prev.searchTerm ? 500 : 0);
-
+
return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter]);
+ }, [filters.searchTerm, filters.statusFilter, filters.priorityFilter, filters.templateTypeFilter, filters.lifecycleFilter]);
// State for backend stats (calculated from entire dataset via SQL queries)
const [backendStats, setBackendStats] = useState<{
@@ -111,7 +116,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
try {
setLoadingStats(true);
-
+
// Use backend stats API - explicitly filter by user's initiator_id
// This ensures "My Requests" only shows requests where user is the initiator
// Even for admin users, we want to see only their own requests in "My Requests"
@@ -131,7 +136,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
undefined, // approverType
filters.searchTerm || undefined,
undefined, // slaCompliance
- true // viewAsUser - treat as normal user even if admin
+ true, // viewAsUser - treat as normal user even if admin
+ filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined // lifecycle
);
setBackendStats({
@@ -149,7 +155,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
} finally {
setLoadingStats(false);
}
- }, [user?.userId, filters.searchTerm, filters.priorityFilter, filters.templateTypeFilter]); // Exclude statusFilter - stats don't change when only status changes
+ }, [user?.userId, filters.searchTerm, filters.priorityFilter, filters.templateTypeFilter, filters.lifecycleFilter]); // Exclude statusFilter - stats don't change when only status changes
// Fetch stats when filters change (excluding status filter)
// Stats should reflect priority and search filters, but NOT status filter
@@ -160,7 +166,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
}, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId);
- }, [filters.searchTerm, filters.priorityFilter, filters.templateTypeFilter, fetchBackendStats]); // Exclude statusFilter - stats don't change when only status changes
+ }, [filters.searchTerm, filters.priorityFilter, filters.templateTypeFilter, filters.lifecycleFilter, fetchBackendStats]); // Exclude statusFilter - stats don't change when only status changes
// Handle dynamic requests (fallback until API loads)
const convertedDynamicRequests = transformRequests(dynamicRequests);
@@ -181,7 +187,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
closed: backendStats.closed || 0,
};
}
-
+
// Fallback: if stats haven't loaded yet, show zeros
return {
total: 0,
@@ -204,6 +210,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
priority: filters.priorityFilter !== 'all' ? filters.priorityFilter : undefined,
templateType: filters.templateTypeFilter !== 'all' ? filters.templateTypeFilter : undefined,
+ lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
});
}
},
@@ -226,8 +233,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
/>
{/* Stats Overview */}
-
{
filters.setStatusFilter(status);
}}
@@ -243,6 +250,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
onStatusChange={filters.setStatusFilter}
onPriorityChange={filters.setPriorityFilter}
onTemplateTypeChange={filters.setTemplateTypeFilter}
+ lifecycleFilter={filters.lifecycleFilter}
+ onLifecycleChange={filters.setLifecycleFilter}
/>
{/* Requests List */}
diff --git a/src/pages/MyRequests/components/MyRequestsFilters.tsx b/src/pages/MyRequests/components/MyRequestsFilters.tsx
index c1a0cc4..bd5c2f6 100644
--- a/src/pages/MyRequests/components/MyRequestsFilters.tsx
+++ b/src/pages/MyRequests/components/MyRequestsFilters.tsx
@@ -12,10 +12,12 @@ interface MyRequestsFiltersProps {
statusFilter: string;
priorityFilter: string;
templateTypeFilter: string;
+ lifecycleFilter: string;
onSearchChange: (value: string) => void;
onStatusChange: (value: string) => void;
onPriorityChange: (value: string) => void;
onTemplateTypeChange: (value: string) => void;
+ onLifecycleChange: (value: string) => void;
}
export function MyRequestsFilters({
@@ -23,10 +25,12 @@ export function MyRequestsFilters({
statusFilter,
priorityFilter,
// templateTypeFilter,
+ lifecycleFilter, // Destructure new prop
onSearchChange,
onStatusChange,
onPriorityChange,
// onTemplateTypeChange,
+ onLifecycleChange, // Destructure new prop
}: MyRequestsFiltersProps) {
return (
@@ -44,6 +48,21 @@ export function MyRequestsFilters({
+ {/* Lifecycle Filter */}
+
+
diff --git a/src/pages/MyRequests/components/MyRequestsStats.tsx b/src/pages/MyRequests/components/MyRequestsStats.tsx
index a4cbdc5..cbd4573 100644
--- a/src/pages/MyRequests/components/MyRequestsStats.tsx
+++ b/src/pages/MyRequests/components/MyRequestsStats.tsx
@@ -2,7 +2,7 @@
* My Requests Stats Section Component
*/
-import { FileText, Clock, Pause, CheckCircle, XCircle, Edit, Archive } from 'lucide-react';
+import { FileText, Clock, Pause, CheckCircle, XCircle, Edit } from 'lucide-react';
import { StatsCard } from '@/components/dashboard/StatsCard';
import { MyRequestsStats } from '../types/myRequests.types';
@@ -18,7 +18,7 @@ export function MyRequestsStatsSection({ stats, onStatusFilter }: MyRequestsStat
}
};
return (
-
+
handleCardClick('draft') : undefined}
/>
-
- handleCardClick('closed') : undefined}
- />
);
}
diff --git a/src/pages/MyRequests/components/RequestCard.tsx b/src/pages/MyRequests/components/RequestCard.tsx
index 9e36fd0..5bd0159 100644
--- a/src/pages/MyRequests/components/RequestCard.tsx
+++ b/src/pages/MyRequests/components/RequestCard.tsx
@@ -8,7 +8,7 @@ import { Badge } from '@/components/ui/badge';
import { ArrowRight, User, TrendingUp, Clock, Pause } from 'lucide-react';
import { motion } from 'framer-motion';
import { MyRequest } from '../types/myRequests.types';
-import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
+import { getPriorityConfig, getStatusConfig, getWorkflowStateConfig } from '../utils/configMappers';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
/**
@@ -16,23 +16,23 @@ import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
*/
const stripHtmlTags = (html: string): string => {
if (!html) return '';
-
+
// Check if we're in a browser environment
if (typeof document === 'undefined') {
// Fallback for SSR: use regex to strip HTML tags
return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
}
-
+
// Create a temporary div to parse HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
-
+
// Get text content (automatically strips HTML tags)
let text = tempDiv.textContent || tempDiv.innerText || '';
-
+
// Clean up extra whitespace
text = text.replace(/\s+/g, ' ').trim();
-
+
return text;
};
@@ -44,6 +44,7 @@ interface RequestCardProps {
export function RequestCard({ request, index, onViewRequest }: RequestCardProps) {
const statusConfig = getStatusConfig(request.status);
+ const stateConfig = getWorkflowStateConfig(request.workflowState || (request.status === 'draft' ? 'DRAFT' : 'OPEN'));
const priorityConfig = getPriorityConfig(request.priority);
const StatusIcon = statusConfig.icon;
const PriorityIcon = priorityConfig.icon;
@@ -79,6 +80,15 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
{request.status}
+ {stateConfig.label.toLowerCase() !== request.status.toLowerCase() && (
+
+ {stateConfig.label}
+
+ )}
{(request.pauseInfo?.isPaused || (request as any).isPaused) && (
{
const templateType = request?.templateType || (request as any)?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || '';
-
+
// Direct mapping from templateType
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
-
+
if (templateTypeUpper === 'DEALER CLAIM') {
templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template';
}
-
+
return (
{
+ async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; templateType?: string, lifecycle?: string }) => {
try {
if (page === 1) {
setLoading(true);
@@ -43,6 +44,7 @@ export function useMyRequests({ itemsPerPage = 10 }: UseMyRequestsOptions = {})
status: filters?.status,
priority: filters?.priority,
templateType: filters?.templateType,
+ lifecycle: filters?.lifecycle,
});
// Extract data - workflowApi now returns { data: [], pagination: {} }
diff --git a/src/pages/MyRequests/hooks/useMyRequestsFilters.ts b/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
index bc99254..1634650 100644
--- a/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
+++ b/src/pages/MyRequests/hooks/useMyRequestsFilters.ts
@@ -11,6 +11,7 @@ import {
setPriorityFilter as setPriorityFilterAction,
setTemplateTypeFilter as setTemplateTypeFilterAction,
setCurrentPage as setCurrentPageAction,
+ setLifecycleFilter as setLifecycleFilterAction,
clearFilters as clearFiltersAction,
} from '../redux/myRequestsSlice';
@@ -23,16 +24,17 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
const dispatch = useAppDispatch();
const debounceTimeoutRef = useRef(null);
const isInitialMount = useRef(true);
-
+
// Get filters from Redux
- const { searchTerm, statusFilter, priorityFilter, templateTypeFilter, currentPage } = useAppSelector((state) => state.myRequests);
-
+ const { searchTerm, statusFilter, priorityFilter, templateTypeFilter, currentPage, lifecycleFilter } = useAppSelector((state) => state.myRequests);
+
// Create setters that dispatch Redux actions
const setSearchTerm = useCallback((value: string) => dispatch(setSearchTermAction(value)), [dispatch]);
const setStatusFilter = useCallback((value: string) => dispatch(setStatusFilterAction(value)), [dispatch]);
const setPriorityFilter = useCallback((value: string) => dispatch(setPriorityFilterAction(value)), [dispatch]);
const setTemplateTypeFilter = useCallback((value: string) => dispatch(setTemplateTypeFilterAction(value)), [dispatch]);
const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
+ const setLifecycleFilter = useCallback((value: string) => dispatch(setLifecycleFilterAction(value)), [dispatch]);
const getFilters = useCallback((): MyRequestsFilters => {
return {
@@ -40,8 +42,9 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
status: statusFilter,
priority: priorityFilter,
templateType: templateTypeFilter,
+ lifecycle: lifecycleFilter,
};
- }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter]);
+ }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, lifecycleFilter]);
// Debounced filter change handler
useEffect(() => {
@@ -50,7 +53,7 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
isInitialMount.current = false;
return;
}
-
+
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
@@ -68,7 +71,7 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
clearTimeout(debounceTimeoutRef.current);
}
};
- }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, onFiltersChange, getFilters, debounceMs]);
+ }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, lifecycleFilter, onFiltersChange, getFilters, debounceMs]);
const resetFilters = useCallback(() => {
dispatch(clearFiltersAction());
@@ -80,11 +83,13 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
priorityFilter,
templateTypeFilter,
currentPage,
+ lifecycleFilter,
setSearchTerm,
setStatusFilter,
setPriorityFilter,
setTemplateTypeFilter,
setCurrentPage,
+ setLifecycleFilter,
getFilters,
resetFilters,
};
diff --git a/src/pages/MyRequests/redux/myRequestsSlice.ts b/src/pages/MyRequests/redux/myRequestsSlice.ts
index 54b4385..f4c0e8c 100644
--- a/src/pages/MyRequests/redux/myRequestsSlice.ts
+++ b/src/pages/MyRequests/redux/myRequestsSlice.ts
@@ -6,6 +6,7 @@ export interface MyRequestsFiltersState {
priorityFilter: string;
templateTypeFilter: string;
currentPage: number;
+ lifecycleFilter: string;
}
const initialState: MyRequestsFiltersState = {
@@ -14,6 +15,7 @@ const initialState: MyRequestsFiltersState = {
priorityFilter: 'all',
templateTypeFilter: 'all',
currentPage: 1,
+ lifecycleFilter: 'all',
};
const myRequestsSlice = createSlice({
@@ -37,12 +39,16 @@ const myRequestsSlice = createSlice({
setCurrentPage: (state, action: PayloadAction) => {
state.currentPage = action.payload;
},
+ setLifecycleFilter: (state, action: PayloadAction) => {
+ state.lifecycleFilter = action.payload;
+ },
clearFilters: (state) => {
state.searchTerm = '';
state.statusFilter = 'all';
state.priorityFilter = 'all';
state.templateTypeFilter = 'all';
state.currentPage = 1;
+ state.lifecycleFilter = 'all';
},
},
});
@@ -53,6 +59,7 @@ export const {
setPriorityFilter,
setTemplateTypeFilter,
setCurrentPage,
+ setLifecycleFilter,
clearFilters,
} = myRequestsSlice.actions;
diff --git a/src/pages/MyRequests/types/myRequests.types.ts b/src/pages/MyRequests/types/myRequests.types.ts
index 8dc2397..0901612 100644
--- a/src/pages/MyRequests/types/myRequests.types.ts
+++ b/src/pages/MyRequests/types/myRequests.types.ts
@@ -17,6 +17,7 @@ export interface MyRequest {
approverLevel?: string;
templateType?: string;
workflowType?: string;
+ workflowState?: string;
templateName?: string;
pauseInfo?: {
isPaused: boolean;
@@ -41,6 +42,7 @@ export interface MyRequestsFilters {
status: string;
priority: string;
templateType?: string;
+ lifecycle?: string;
}
export interface PaginationState {
diff --git a/src/pages/MyRequests/utils/configMappers.ts b/src/pages/MyRequests/utils/configMappers.ts
index f35f1d8..6c91098 100644
--- a/src/pages/MyRequests/utils/configMappers.ts
+++ b/src/pages/MyRequests/utils/configMappers.ts
@@ -87,3 +87,25 @@ export function getStatusConfig(status: string): StatusConfig {
}
}
+export function getWorkflowStateConfig(state: string) {
+ const s = (state || '').toUpperCase();
+ switch (s) {
+ case 'CLOSED':
+ return {
+ color: 'bg-slate-100 text-slate-800 border-slate-200',
+ label: 'closed',
+ };
+ case 'DRAFT':
+ return {
+ color: 'bg-gray-100 text-gray-800 border-gray-200',
+ label: 'draft',
+ };
+ case 'OPEN':
+ default:
+ return {
+ color: 'bg-blue-100 text-blue-800 border-blue-200',
+ label: 'open',
+ };
+ }
+}
+
diff --git a/src/pages/MyRequests/utils/requestTransformers.ts b/src/pages/MyRequests/utils/requestTransformers.ts
index 558a878..2f77f9a 100644
--- a/src/pages/MyRequests/utils/requestTransformers.ts
+++ b/src/pages/MyRequests/utils/requestTransformers.ts
@@ -31,6 +31,7 @@ export function transformRequest(req: any): MyRequest {
: 'โ',
templateType: req.templateType || req.template_type,
workflowType: req.workflowType || req.workflow_type,
+ workflowState: req.workflowState || req.workflow_state,
templateName: req.templateName || req.template_name,
};
}
diff --git a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
index 0d26f2b..1afe1a1 100644
--- a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
+++ b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx
@@ -57,20 +57,20 @@ export function QuickActionsSidebar({
const [sharedRecipients, setSharedRecipients] = useState([]);
const [loadingRecipients, setLoadingRecipients] = useState(false);
const [hasRetriggerNotification, setHasRetriggerNotification] = useState(false);
- const isClosed = request?.status === 'closed';
+ const isClosed = apiRequest?.workflowState === 'CLOSED' || request?.status === 'closed';
const isPaused = request?.pauseInfo?.isPaused || false;
const pausedByUserId = pausedByUserIdProp || request?.pauseInfo?.pausedBy?.userId;
const currentUserId = currentUserIdProp || (user as any)?.userId || '';
-
+
// Both approver AND initiator can pause (when not already paused and not closed)
const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator);
-
+
// Resume: Can be done by the person who paused OR by both initiator and approver
const canResume = isPaused && onResume && (currentApprovalLevel || isInitiator);
-
+
// Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger;
-
+
// Check for retrigger notification (initiator requested resume)
// ONLY check when: 1) Request is paused, 2) Current user is an approver
// This avoids unnecessary API calls for non-paused requests or initiators
@@ -80,26 +80,26 @@ export function QuickActionsSidebar({
setHasRetriggerNotification(false);
return;
}
-
+
const checkRetriggerNotification = async () => {
try {
const response = await notificationApi.list({ page: 1, limit: 50, unreadOnly: true }); // Only unread
const notifications: Notification[] = response.data?.notifications || [];
-
+
// Check if there's an UNREAD pause_retrigger_request notification for this request
const hasRetrigger = notifications.some(
- (notif: Notification) =>
- notif.requestId === request.requestId &&
+ (notif: Notification) =>
+ notif.requestId === request.requestId &&
notif.notificationType === 'pause_retrigger_request'
);
-
+
setHasRetriggerNotification(hasRetrigger);
} catch (error) {
console.error('Failed to check retrigger notifications:', error);
setHasRetriggerNotification(false);
}
};
-
+
checkRetriggerNotification();
}, [isPaused, currentApprovalLevel, request?.requestId, refreshTrigger]); // Only when paused state or approver status changes
@@ -332,7 +332,7 @@ export function QuickActionsSidebar({
.join('')
.slice(0, 2)
.toUpperCase();
-
+
return (
diff --git a/src/pages/RequestDetail/components/RequestDetailHeader.tsx b/src/pages/RequestDetail/components/RequestDetailHeader.tsx
index 2a2f7b6..463dc59 100644
--- a/src/pages/RequestDetail/components/RequestDetailHeader.tsx
+++ b/src/pages/RequestDetail/components/RequestDetailHeader.tsx
@@ -5,7 +5,7 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, FileText, RefreshCw, Share2 } from 'lucide-react';
-import { getPriorityConfig, getStatusConfig } from '@/utils/requestDetailHelpers';
+import { getPriorityConfig, getStatusConfig, getWorkflowStateConfig } from '@/utils/requestDetailHelpers';
import { SLAProgressBar } from '@/components/sla/SLAProgressBar';
interface RequestDetailHeaderProps {
@@ -20,18 +20,19 @@ interface RequestDetailHeaderProps {
isPaused?: boolean; // Pass pause status from module
}
-export function RequestDetailHeader({
- request,
- refreshing,
- onBack,
- onRefresh,
- onShareSummary,
+export function RequestDetailHeader({
+ request,
+ refreshing,
+ onBack,
+ onRefresh,
+ onShareSummary,
isInitiator,
slaData, // Module passes prepared SLA data
isPaused = false // Module passes pause status
}: RequestDetailHeaderProps) {
const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending');
+ const stateConfig = getWorkflowStateConfig(request?.workflowState || (request?.status === 'DRAFT' ? 'DRAFT' : 'OPEN'));
return (
@@ -77,31 +78,40 @@ export function RequestDetailHeader({
>
{statusConfig.label}
+ {stateConfig.label.toLowerCase() !== (request?.status || '').toLowerCase() && (
+
+ {stateConfig.label}
+
+ )}
{/* Template Type Badge */}
{(() => {
const workflowType = request?.workflowType || request?.workflow_type;
const templateType = request?.templateType || request?.template_type || '';
const templateTypeUpper = templateType?.toUpperCase() || '';
-
+
// Check for dealer claim - support multiple formats
- const isDealerClaim =
- workflowType === 'CLAIM_MANAGEMENT' ||
+ const isDealerClaim =
+ workflowType === 'CLAIM_MANAGEMENT' ||
workflowType === 'DEALER_CLAIM' ||
templateType === 'claim-management' ||
templateTypeUpper === 'DEALER CLAIM' ||
templateTypeUpper === 'DEALER_CLAIM';
-
+
// Direct mapping from templateType
let templateLabel = 'Non-Templatized';
let templateColor = 'bg-purple-100 !text-purple-600 border-purple-200';
-
+
if (isDealerClaim) {
templateLabel = 'Dealer Claim';
templateColor = 'bg-blue-100 !text-blue-700 border-blue-200';
} else if (templateTypeUpper === 'TEMPLATE') {
templateLabel = 'Template';
}
-
+
return (
{/* Share Summary Button - Only show for closed requests if user is initiator */}
- {onShareSummary && isInitiator && request?.status?.toLowerCase() === 'closed' && (
+ {onShareSummary && isInitiator && request?.workflowState?.toLowerCase() === 'closed' && (
)}
diff --git a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
index a1b95b7..ef6b5ac 100644
--- a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
+++ b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx
@@ -35,6 +35,7 @@ interface OverviewTabProps {
generationAttempts?: number;
generationFailed?: boolean;
maxAttemptsReached?: boolean;
+ isClosed?: boolean;
}
export function OverviewTab({
@@ -57,6 +58,7 @@ export function OverviewTab({
generationAttempts = 0,
generationFailed = false,
maxAttemptsReached = false,
+ isClosed = false,
}: OverviewTabProps) {
void _onPause; // Marked as intentionally unused - available for future use
const { user } = useAuth();
@@ -64,10 +66,10 @@ export function OverviewTab({
const isPaused = pauseInfo?.isPaused || false;
const pausedByUserId = pauseInfo?.pausedBy?.userId;
const currentUserId = (user as any)?.userId || '';
-
+
// Resume: Can be done by both initiator and approver
const canResume = isPaused && onResume && (currentUserIsApprover || isInitiator);
-
+
// Retrigger: Only for initiator when approver paused (initiator asks approver to resume)
const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger;
@@ -122,8 +124,8 @@ export function OverviewTab({
-
@@ -187,14 +189,14 @@ export function OverviewTab({
{pauseInfo.pauseReason}
)}
-
+
{pauseInfo.pausedBy && (
{pauseInfo.pausedBy.name || pauseInfo.pausedBy.email}
)}
-
+
{pauseInfo.pauseResumeDate && (
@@ -208,7 +210,7 @@ export function OverviewTab({
)}
-
+
{pauseInfo.pausedAt && (
@@ -289,8 +291,8 @@ export function OverviewTab({
-
@@ -301,7 +303,7 @@ export function OverviewTab({
)}
{/* Read-Only Conclusion Remark */}
- {request.status === 'closed' && request.conclusionRemark && (
+ {isClosed && request.conclusionRemark && (
@@ -312,8 +314,8 @@ export function OverviewTab({
-
@@ -331,23 +333,20 @@ export function OverviewTab({
{/* Conclusion Remark Section */}
{needsClosure && (
-
+ }`}>
-
-
+
+
Conclusion Remark - Final Step
- {request.status === 'rejected'
+ {request.status === 'rejected'
? 'This request was rejected. Please review the AI-generated closure remark and finalize it to close this request.'
: 'All approvals are complete. Please review and finalize the conclusion to close this request.'}
@@ -365,7 +364,7 @@ export function OverviewTab({
{aiGenerated ? 'Regenerate' : 'Generate with AI'}
{aiGenerated && !maxAttemptsReached && !generationFailed && (
-
+
{2 - generationAttempts} attempts remaining
)}
diff --git a/src/pages/Requests/Requests.tsx b/src/pages/Requests/Requests.tsx
index 05014ca..eca56d4 100644
--- a/src/pages/Requests/Requests.tsx
+++ b/src/pages/Requests/Requests.tsx
@@ -163,7 +163,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
}).length;
const closed = filteredData.filter((r: any) => {
const status = (r.status || '').toString().toUpperCase();
- return status === 'CLOSED';
+ const state = (r.workflowState || '').toString().toUpperCase();
+ return (status === 'CLOSED' || state === 'CLOSED') && status !== 'APPROVED' && status !== 'REJECTED';
}).length;
setBackendStats({
@@ -396,6 +397,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange,
customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate,
+ lifecycleFilter: filters.lifecycleFilter,
isOrgLevel,
});
const hasInitialFetchRun = useRef(false);
@@ -426,6 +428,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
prev.dateRange !== filters.dateRange ||
prev.customStartDate !== filters.customStartDate ||
prev.customEndDate !== filters.customEndDate ||
+ prev.lifecycleFilter !== filters.lifecycleFilter ||
prev.isOrgLevel !== isOrgLevel;
if (!hasChanged) return;
@@ -447,6 +450,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange,
customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate,
+ lifecycleFilter: filters.lifecycleFilter,
isOrgLevel,
};
}, filters.searchTerm !== prev.searchTerm ? 500 : 0);
@@ -466,7 +470,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
filters.approverFilterType,
filters.dateRange,
filters.customStartDate,
- filters.customEndDate
+ filters.customEndDate,
+ filters.lifecycleFilter
]);
// Page change handler
@@ -553,8 +558,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
{/* Primary Filters */}
-
-
+
+
+
+
diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx
index 3f1cfe0..1b14bfe 100644
--- a/src/pages/Requests/UserAllRequests.tsx
+++ b/src/pages/Requests/UserAllRequests.tsx
@@ -58,7 +58,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
// Determine once - use this throughout instead of checking repeatedly
const isDealer = userFilterType === 'DEALER';
-
+
// Helper to get filters for API - excludes dealer-restricted filters
// Since we know user type initially, this helper uses that knowledge
const getFiltersForApi = useCallback(() => {
@@ -70,7 +70,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
}
return filterOptions;
}, [filters, isDealer]);
-
+
// Helper to calculate active filters count based on user type
const calculateActiveFiltersCount = useCallback(() => {
if (isDealer) {
@@ -120,16 +120,16 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
// OPTIMIZED: Uses backend stats API instead of fetching 100 records
// Stats reflect all filters EXCEPT status - total stays stable when only status changes
const fetchBackendStats = useCallback(async (
- statsDateRange?: DateRange,
- statsStartDate?: Date,
+ statsDateRange?: DateRange,
+ statsStartDate?: Date,
statsEndDate?: Date,
- filtersWithoutStatus?: {
- priority?: string;
+ filtersWithoutStatus?: {
+ priority?: string;
templateType?: string;
- department?: string;
- initiator?: string;
- approver?: string;
- approverType?: 'current' | 'any';
+ department?: string;
+ initiator?: string;
+ approver?: string;
+ approverType?: 'current' | 'any';
search?: string;
slaCompliance?: string;
}
@@ -199,7 +199,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
const filtersRef = useRef(filters);
const fetchBackendStatsRef = useRef(fetchBackendStats);
const getFiltersForApiRef = useRef(getFiltersForApi);
-
+
// Update refs on each render
useEffect(() => {
filtersRef.current = filters;
@@ -275,7 +275,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
approverType: filters.approverFilter !== 'all' ? filters.approverFilterType : undefined,
search: filters.searchTerm || undefined,
};
-
+
// Only include priority, templateType, department, and slaCompliance if user is not a dealer
if (!isDealer) {
if (filters.priorityFilter !== 'all') filtersWithoutStatus.priority = filters.priorityFilter;
@@ -283,13 +283,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
if (filters.departmentFilter !== 'all') filtersWithoutStatus.department = filters.departmentFilter;
if (filters.slaComplianceFilter !== 'all') filtersWithoutStatus.slaCompliance = filters.slaComplianceFilter;
}
-
+
// Use 'all' if dateRange is 'all', otherwise use the selected dateRange or default to 'month'
const statsDateRange = filters.dateRange === 'all' ? 'all' : (filters.dateRange || 'month');
-
+
fetchBackendStatsRef.current(
- statsDateRange,
- filters.customStartDate,
+ statsDateRange,
+ filters.customStartDate,
filters.customEndDate,
filtersWithoutStatus
);
@@ -327,9 +327,10 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange,
customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate,
+ lifecycleFilter: filters.lifecycleFilter,
});
const hasInitialFetchRun = useRef(false);
-
+
// Initial fetch on mount - use stored page from Redux
useEffect(() => {
const storedPage = filters.currentPage || 1;
@@ -337,13 +338,13 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
hasInitialFetchRun.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only on mount
-
+
// Fetch when filters change
useEffect(() => {
if (!hasInitialFetchRun.current) return;
-
+
const prev = prevFiltersRef.current;
- const hasChanged =
+ const hasChanged =
prev.searchTerm !== filters.searchTerm ||
prev.statusFilter !== filters.statusFilter ||
prev.priorityFilter !== filters.priorityFilter ||
@@ -355,14 +356,15 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
prev.approverFilterType !== filters.approverFilterType ||
prev.dateRange !== filters.dateRange ||
prev.customStartDate !== filters.customStartDate ||
- prev.customEndDate !== filters.customEndDate;
-
+ prev.customEndDate !== filters.customEndDate ||
+ prev.lifecycleFilter !== filters.lifecycleFilter;
+
if (!hasChanged) return;
-
+
const timeoutId = setTimeout(() => {
filters.setCurrentPage(1);
fetchRequests(1);
-
+
prevFiltersRef.current = {
searchTerm: filters.searchTerm,
statusFilter: filters.statusFilter,
@@ -376,6 +378,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange,
customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate,
+ lifecycleFilter: filters.lifecycleFilter,
};
}, filters.searchTerm !== prev.searchTerm ? 500 : 0);
@@ -393,7 +396,9 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
filters.approverFilterType,
filters.dateRange,
filters.customStartDate,
- filters.customEndDate
+ filters.customStartDate,
+ filters.customEndDate,
+ filters.lifecycleFilter
]);
// Page change handler
@@ -406,7 +411,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
// Transform requests
const convertedRequests = useMemo(() => transformRequests(apiRequests), [apiRequests]);
-
+
// Calculate stats - Use backend stats API (OPTIMIZED)
const stats = useMemo(() => {
// Use backend stats if available
@@ -421,38 +426,38 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
closed: backendStats.closed || 0
};
}
-
+
// Fallback: calculate from current page (less accurate, but works during initial load)
- const pending = convertedRequests.filter((r: any) => {
- const status = (r.status || '').toString().toLowerCase();
- return status === 'pending' || status === 'in-progress';
- }).length;
+ const pending = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'pending' || status === 'in-progress';
+ }).length;
const paused = convertedRequests.filter((r: any) => {
const status = (r.status || '').toString().toLowerCase();
return status === 'paused';
}).length;
- const approved = convertedRequests.filter((r: any) => {
- const status = (r.status || '').toString().toLowerCase();
- return status === 'approved';
- }).length;
- const rejected = convertedRequests.filter((r: any) => {
- const status = (r.status || '').toString().toLowerCase();
- return status === 'rejected';
- }).length;
- const closed = convertedRequests.filter((r: any) => {
- const status = (r.status || '').toString().toLowerCase();
- return status === 'closed';
- }).length;
-
- return {
+ const approved = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'approved';
+ }).length;
+ const rejected = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'rejected';
+ }).length;
+ const closed = convertedRequests.filter((r: any) => {
+ const status = (r.status || '').toString().toLowerCase();
+ return status === 'closed';
+ }).length;
+
+ return {
total: totalRecords > 0 ? totalRecords : convertedRequests.length,
- pending,
+ pending,
paused,
- approved,
- rejected,
- draft: 0,
- closed
- };
+ approved,
+ rejected,
+ draft: 0,
+ closed
+ };
}, [backendStats, totalRecords, convertedRequests]);
return (
@@ -467,8 +472,8 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
/>
{/* Stats */}
-
{
filters.setStatusFilter(status);
}}
@@ -477,6 +482,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
{/* Filters - Plug-and-play pattern */}
{request.status}
+ {stateConfig.label.toLowerCase() !== (request.status || '').toLowerCase() && (
+
+ {stateConfig.label}
+
+ )}
{((request as any).pauseInfo?.isPaused || (request as any).isPaused) && (
+
handleCardClick('rejected') : undefined}
/>
-
- handleCardClick('closed') : undefined}
- />
);
}
diff --git a/src/pages/Requests/hooks/useRequestsFilters.ts b/src/pages/Requests/hooks/useRequestsFilters.ts
index 5f0ca92..70a23c9 100644
--- a/src/pages/Requests/hooks/useRequestsFilters.ts
+++ b/src/pages/Requests/hooks/useRequestsFilters.ts
@@ -22,6 +22,7 @@ import {
setCustomEndDate as setCustomEndDateAction,
setShowCustomDatePicker as setShowCustomDatePickerAction,
setCurrentPage as setCurrentPageAction,
+ setLifecycleFilter as setLifecycleFilterAction,
clearFilters as clearFiltersAction,
} from '../redux/requestsSlice';
@@ -44,6 +45,7 @@ export function useRequestsFilters() {
customEndDate,
showCustomDatePicker,
currentPage,
+ lifecycleFilter,
} = useAppSelector((state) => state.requests);
// Create setters that dispatch Redux actions
@@ -61,6 +63,7 @@ export function useRequestsFilters() {
const setCustomEndDate = useCallback((value: Date | undefined) => dispatch(setCustomEndDateAction(value)), [dispatch]);
const setShowCustomDatePicker = useCallback((value: boolean) => dispatch(setShowCustomDatePickerAction(value)), [dispatch]);
const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
+ const setLifecycleFilter = useCallback((value: string) => dispatch(setLifecycleFilterAction(value)), [dispatch]);
const getFilters = useCallback((): RequestFilters => {
return {
@@ -73,6 +76,7 @@ export function useRequestsFilters() {
initiator: initiatorFilter !== 'all' ? initiatorFilter : undefined,
approver: approverFilter !== 'all' ? approverFilter : undefined,
approverType: approverFilter !== 'all' ? approverFilterType : undefined,
+ lifecycle: lifecycleFilter !== 'all' ? lifecycleFilter : undefined,
dateRange,
startDate: customStartDate,
endDate: customEndDate
@@ -87,6 +91,7 @@ export function useRequestsFilters() {
initiatorFilter,
approverFilter,
approverFilterType,
+ lifecycleFilter, // Ensure lifecycleFilter is in dependencies
dateRange,
customStartDate,
customEndDate
@@ -128,6 +133,7 @@ export function useRequestsFilters() {
departmentFilter !== 'all' ||
initiatorFilter !== 'all' ||
approverFilter !== 'all' ||
+ lifecycleFilter !== 'all' ||
dateRange !== 'all' ||
customStartDate ||
customEndDate
@@ -147,6 +153,7 @@ export function useRequestsFilters() {
dateRange,
customStartDate,
customEndDate,
+ lifecycleFilter,
showCustomDatePicker,
currentPage,
hasActiveFilters,
@@ -165,6 +172,7 @@ export function useRequestsFilters() {
setCustomEndDate,
setShowCustomDatePicker,
setCurrentPage,
+ setLifecycleFilter,
// Helpers
getFilters,
clearFilters,
diff --git a/src/pages/Requests/redux/requestsSlice.ts b/src/pages/Requests/redux/requestsSlice.ts
index 65f9fff..c24c28a 100644
--- a/src/pages/Requests/redux/requestsSlice.ts
+++ b/src/pages/Requests/redux/requestsSlice.ts
@@ -16,6 +16,7 @@ export interface RequestsFiltersState {
customEndDate?: Date;
showCustomDatePicker: boolean;
currentPage: number;
+ lifecycleFilter: string;
}
const initialState: RequestsFiltersState = {
@@ -33,6 +34,7 @@ const initialState: RequestsFiltersState = {
customEndDate: undefined,
showCustomDatePicker: false,
currentPage: 1,
+ lifecycleFilter: 'all',
};
const requestsSlice = createSlice({
@@ -81,6 +83,9 @@ const requestsSlice = createSlice({
setCurrentPage: (state, action: PayloadAction) => {
state.currentPage = action.payload;
},
+ setLifecycleFilter: (state, action: PayloadAction) => {
+ state.lifecycleFilter = action.payload;
+ },
clearFilters: (state) => {
state.searchTerm = '';
state.statusFilter = 'all';
@@ -96,6 +101,7 @@ const requestsSlice = createSlice({
state.customEndDate = undefined;
state.showCustomDatePicker = false;
state.currentPage = 1;
+ state.lifecycleFilter = 'all';
},
},
});
@@ -115,6 +121,7 @@ export const {
setCustomEndDate,
setShowCustomDatePicker,
setCurrentPage,
+ setLifecycleFilter,
clearFilters,
} = requestsSlice.actions;
diff --git a/src/pages/Requests/services/requestsService.ts b/src/pages/Requests/services/requestsService.ts
index 4a39e65..8337673 100644
--- a/src/pages/Requests/services/requestsService.ts
+++ b/src/pages/Requests/services/requestsService.ts
@@ -36,6 +36,7 @@ export async function fetchRequestsData({
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString();
if (filters?.endDate) backendFilters.endDate = filters.endDate?.toISOString();
+ if (filters?.lifecycle && filters.lifecycle !== 'all') backendFilters.lifecycle = filters.lifecycle;
// Fetch paginated data for list display (with status filter)
const pageResult = await workflowApi.listWorkflows({
@@ -81,60 +82,61 @@ export async function fetchRequestsData({
totalPages: pagination.totalPages || 1
}
};
- } else {
- // User-level: Use SEPARATE endpoint for regular users' "All Requests" page
- // This shows ALL requests where user is involved:
- // - As initiator (created the request)
- // - As approver (in any approval level)
- // - As participant/spectator
- const backendFilters: any = {};
- if (filters?.search) backendFilters.search = filters.search;
- if (filters?.status && filters.status !== 'all') backendFilters.status = filters.status;
- if (filters?.priority && filters.priority !== 'all') backendFilters.priority = filters.priority;
- if (filters?.templateType && filters.templateType !== 'all') backendFilters.templateType = filters.templateType;
- if (filters?.department && filters.department !== 'all') backendFilters.department = filters.department;
- if (filters?.initiator && filters.initiator !== 'all') backendFilters.initiator = filters.initiator;
- if (filters?.slaCompliance && filters.slaCompliance !== 'all') backendFilters.slaCompliance = filters.slaCompliance;
- if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
- if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString();
- if (filters?.endDate) backendFilters.endDate = filters.endDate?.toISOString();
+ } else {
+ // User-level: Use SEPARATE endpoint for regular users' "All Requests" page
+ // This shows ALL requests where user is involved:
+ // - As initiator (created the request)
+ // - As approver (in any approval level)
+ // - As participant/spectator
+ const backendFilters: any = {};
+ if (filters?.search) backendFilters.search = filters.search;
+ if (filters?.status && filters.status !== 'all') backendFilters.status = filters.status;
+ if (filters?.priority && filters.priority !== 'all') backendFilters.priority = filters.priority;
+ if (filters?.templateType && filters.templateType !== 'all') backendFilters.templateType = filters.templateType;
+ if (filters?.department && filters.department !== 'all') backendFilters.department = filters.department;
+ if (filters?.initiator && filters.initiator !== 'all') backendFilters.initiator = filters.initiator;
+ if (filters?.slaCompliance && filters.slaCompliance !== 'all') backendFilters.slaCompliance = filters.slaCompliance;
+ if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
+ if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString();
+ if (filters?.endDate) backendFilters.endDate = filters.endDate?.toISOString();
+ if (filters?.lifecycle && filters.lifecycle !== 'all') backendFilters.lifecycle = filters.lifecycle;
- // Fetch paginated data using endpoint for regular users
- // This endpoint includes all requests where user is initiator, approver, or participant
- const pageResult = await workflowApi.listParticipantRequests({
- page,
- limit: itemsPerPage,
- ...backendFilters
- });
+ // Fetch paginated data using endpoint for regular users
+ // This endpoint includes all requests where user is initiator, approver, or participant
+ const pageResult = await workflowApi.listParticipantRequests({
+ page,
+ limit: itemsPerPage,
+ ...backendFilters
+ });
- let pageData: any[] = [];
- if (Array.isArray(pageResult?.data)) {
- pageData = pageResult.data;
- } else if (Array.isArray(pageResult)) {
- pageData = pageResult;
- }
-
- // Filter out drafts (backend should handle this, but double-check)
- const nonDraftData = pageData.filter((req: any) => {
- const reqStatus = (req.status || '').toString().toUpperCase();
- return reqStatus !== 'DRAFT';
- });
-
- // Get pagination info from backend response
- const pagination = pageResult?.pagination || {
- page,
- limit: itemsPerPage,
- total: nonDraftData.length,
- totalPages: 1
- };
-
- return {
- data: nonDraftData, // Paginated data for list
- allData: [], // Stats come from backend stats API for user-level too
- filteredData: nonDraftData, // This is the data for the current page, already filtered
- pagination: pagination
- };
+ let pageData: any[] = [];
+ if (Array.isArray(pageResult?.data)) {
+ pageData = pageResult.data;
+ } else if (Array.isArray(pageResult)) {
+ pageData = pageResult;
}
+
+ // Filter out drafts (backend should handle this, but double-check)
+ const nonDraftData = pageData.filter((req: any) => {
+ const reqStatus = (req.status || '').toString().toUpperCase();
+ return reqStatus !== 'DRAFT';
+ });
+
+ // Get pagination info from backend response
+ const pagination = pageResult?.pagination || {
+ page,
+ limit: itemsPerPage,
+ total: nonDraftData.length,
+ totalPages: 1
+ };
+
+ return {
+ data: nonDraftData, // Paginated data for list
+ allData: [], // Stats come from backend stats API for user-level too
+ filteredData: nonDraftData, // This is the data for the current page, already filtered
+ pagination: pagination
+ };
+ }
}
export async function fetchAllRequestsForExport(isOrgLevel: boolean): Promise {
diff --git a/src/pages/Requests/services/userRequestsService.ts b/src/pages/Requests/services/userRequestsService.ts
index a54a613..b164cf7 100644
--- a/src/pages/Requests/services/userRequestsService.ts
+++ b/src/pages/Requests/services/userRequestsService.ts
@@ -41,6 +41,7 @@ export async function fetchUserParticipantRequestsData({
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate;
if (filters?.endDate) backendFilters.endDate = filters.endDate instanceof Date ? filters.endDate.toISOString() : filters.endDate;
+ if (filters?.lifecycle && filters.lifecycle !== 'all') backendFilters.lifecycle = filters.lifecycle;
// Use single optimized endpoint - listParticipantRequests now includes initiator requests
// Only fetch the requested page (10 records) for optimal performance
@@ -113,6 +114,7 @@ export async function fetchAllRequestsForExport(filters?: RequestFilters): Promi
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate;
if (filters?.endDate) backendFilters.endDate = filters.endDate instanceof Date ? filters.endDate.toISOString() : filters.endDate;
+ if (filters?.lifecycle && filters.lifecycle !== 'all') backendFilters.lifecycle = filters.lifecycle;
// Fetch all pages using the single optimized endpoint
while (hasMore && currentPage <= maxPages) {
@@ -150,4 +152,3 @@ export async function fetchAllRequestsForExport(filters?: RequestFilters): Promi
return allPages;
}
-
diff --git a/src/pages/Requests/types/requests.types.ts b/src/pages/Requests/types/requests.types.ts
index 82116f2..6beacb7 100644
--- a/src/pages/Requests/types/requests.types.ts
+++ b/src/pages/Requests/types/requests.types.ts
@@ -21,6 +21,7 @@ export interface RequestFilters {
dateRange?: DateRange;
startDate?: Date;
endDate?: Date;
+ lifecycle?: string;
}
export interface RequestStats {
@@ -64,6 +65,7 @@ export interface ConvertedRequest {
approverLevel: string;
templateType?: string;
workflowType?: string;
+ workflowState?: string;
templateName?: string;
}
diff --git a/src/pages/Requests/utils/configMappers.ts b/src/pages/Requests/utils/configMappers.ts
index 17e0f84..80b7ce4 100644
--- a/src/pages/Requests/utils/configMappers.ts
+++ b/src/pages/Requests/utils/configMappers.ts
@@ -68,3 +68,25 @@ export const getStatusConfig = (status: string) => {
}
};
+export const getWorkflowStateConfig = (state: string) => {
+ const s = (state || '').toUpperCase();
+ switch (s) {
+ case 'CLOSED':
+ return {
+ color: 'bg-slate-100 text-slate-800 border-slate-200',
+ label: 'closed'
+ };
+ case 'DRAFT':
+ return {
+ color: 'bg-gray-100 text-gray-800 border-gray-200',
+ label: 'draft'
+ };
+ case 'OPEN':
+ default:
+ return {
+ color: 'bg-blue-100 text-blue-800 border-blue-200',
+ label: 'open'
+ };
+ }
+};
+
diff --git a/src/pages/Requests/utils/requestTransformers.ts b/src/pages/Requests/utils/requestTransformers.ts
index 9aa0233..2236d12 100644
--- a/src/pages/Requests/utils/requestTransformers.ts
+++ b/src/pages/Requests/utils/requestTransformers.ts
@@ -8,21 +8,21 @@ export function transformRequest(req: any): ConvertedRequest {
const createdAt = req.submittedAt || req.submitted_at || req.createdAt || req.created_at;
const priority = (req.priority || '').toString().toLowerCase();
const status = (req.status || '').toString().toUpperCase();
-
+
// Extract current approver - handle multiple field name variations
let currentApprover = 'โ';
let approverLevel = 'โ';
-
+
// Try to get current approver from various possible locations
const currentApproverObj = req.currentApprover || req.current_approver || req.currentApproverData;
if (currentApproverObj) {
// Handle object format: { name, email, approverName, approverEmail, etc. }
- currentApprover = currentApproverObj.name ||
- currentApproverObj.approverName ||
- currentApproverObj.displayName ||
- currentApproverObj.email ||
- currentApproverObj.approverEmail ||
- 'โ';
+ currentApprover = currentApproverObj.name ||
+ currentApproverObj.approverName ||
+ currentApproverObj.displayName ||
+ currentApproverObj.email ||
+ currentApproverObj.approverEmail ||
+ 'โ';
} else if (req.approvals && Array.isArray(req.approvals) && req.approvals.length > 0) {
// For completed requests, show the last approver (final approver)
// For active requests, find the current pending/in-progress approver
@@ -30,15 +30,15 @@ export function transformRequest(req: any): ConvertedRequest {
const aStatus = (a.status || '').toString().toUpperCase();
return aStatus === 'PENDING' || aStatus === 'IN_PROGRESS';
});
-
+
if (activeApproval) {
// Active request - show current approver
- currentApprover = activeApproval.approverName ||
- activeApproval.approver?.name ||
- activeApproval.approver?.displayName ||
- activeApproval.approverEmail ||
- activeApproval.approver?.email ||
- 'โ';
+ currentApprover = activeApproval.approverName ||
+ activeApproval.approver?.name ||
+ activeApproval.approver?.displayName ||
+ activeApproval.approverEmail ||
+ activeApproval.approver?.email ||
+ 'โ';
} else {
// Completed request - show final approver (last one in the array, or highest level)
const sortedApprovals = [...req.approvals].sort((a: any, b: any) => {
@@ -48,20 +48,20 @@ export function transformRequest(req: any): ConvertedRequest {
});
const finalApproval = sortedApprovals[0];
if (finalApproval) {
- currentApprover = finalApproval.approverName ||
- finalApproval.approver?.name ||
- finalApproval.approver?.displayName ||
- finalApproval.approverEmail ||
- finalApproval.approver?.email ||
- 'โ';
+ currentApprover = finalApproval.approverName ||
+ finalApproval.approver?.name ||
+ finalApproval.approver?.displayName ||
+ finalApproval.approverEmail ||
+ finalApproval.approver?.email ||
+ 'โ';
}
}
}
-
+
// Extract approval level information - handle multiple field name variations
const currentLevel = req.currentLevel || req.current_level || req.currentLevelNumber || req.current_level_number;
const totalLevels = req.totalLevels || req.total_levels || req.totalLevelsCount || req.total_levels_count;
-
+
if (currentLevel && totalLevels) {
approverLevel = `${currentLevel} of ${totalLevels}`;
} else if (req.approvals && Array.isArray(req.approvals) && req.approvals.length > 0) {
@@ -70,7 +70,7 @@ export function transformRequest(req: any): ConvertedRequest {
const aStatus = (a.status || '').toString().toUpperCase();
return aStatus === 'PENDING' || aStatus === 'IN_PROGRESS';
});
-
+
if (activeApproval) {
const levelNum = activeApproval.levelNumber || activeApproval.level_number || 0;
const total = totalLevels || req.approvals.length;
@@ -83,14 +83,14 @@ export function transformRequest(req: any): ConvertedRequest {
// Alternative field names
approverLevel = `${req.currentStep} of ${req.totalSteps}`;
}
-
+
return {
id: req.requestNumber || req.request_number || req.requestId || req.id || req.request_id,
requestId: req.requestId || req.id || req.request_id,
displayId: req.requestNumber || req.request_number || req.id,
title: req.title,
description: req.description,
- status: status.toLowerCase().replace('_','-'),
+ status: status.toLowerCase().replace('_', '-'),
priority: priority,
department: req.department || req.initiator?.department,
submittedDate: req.submittedAt || (req.createdAt ? new Date(req.createdAt).toISOString().split('T')[0] : undefined),
@@ -99,6 +99,7 @@ export function transformRequest(req: any): ConvertedRequest {
approverLevel: approverLevel,
templateType: req.templateType || req.template_type,
workflowType: req.workflowType || req.workflow_type,
+ workflowState: req.workflowState || req.workflow_state,
templateName: req.templateName || req.template_name
};
}
diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts
index 62e2d05..7cd7860 100644
--- a/src/services/dashboard.service.ts
+++ b/src/services/dashboard.service.ts
@@ -102,6 +102,8 @@ export interface CriticalRequest {
isCritical: boolean;
approverId?: string | null;
approverEmail?: string | null;
+ isActionable?: boolean;
+ requestRole?: 'APPROVER' | 'INITIATOR' | 'PARTICIPANT';
}
export interface AIRemarkUtilization {
@@ -203,7 +205,8 @@ class DashboardService {
approverType?: 'current' | 'any',
search?: string,
slaCompliance?: string,
- viewAsUser?: boolean
+ viewAsUser?: boolean,
+ lifecycle?: string
): Promise {
try {
const params: any = { dateRange };
@@ -215,6 +218,9 @@ class DashboardService {
if (status && status !== 'all') {
params.status = status;
}
+ if (lifecycle && lifecycle !== 'all') {
+ params.lifecycle = lifecycle;
+ }
if (priority && priority !== 'all') {
params.priority = priority;
}
@@ -314,8 +320,8 @@ class DashboardService {
/**
* Get recent activity feed with pagination
*/
- async getRecentActivity(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
- activities: RecentActivity[],
+ async getRecentActivity(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
+ activities: RecentActivity[],
pagination: {
currentPage: number,
totalPages: number,
@@ -342,8 +348,8 @@ class DashboardService {
/**
* Get critical requests with pagination
*/
- async getCriticalRequests(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
- criticalRequests: CriticalRequest[],
+ async getCriticalRequests(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
+ criticalRequests: CriticalRequest[],
pagination: {
currentPage: number,
totalPages: number,
@@ -370,8 +376,8 @@ class DashboardService {
/**
* Get upcoming deadlines with pagination
*/
- async getUpcomingDeadlines(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
- deadlines: UpcomingDeadline[],
+ async getUpcomingDeadlines(page: number = 1, limit: number = 10, viewAsUser?: boolean): Promise<{
+ deadlines: UpcomingDeadline[],
pagination: {
currentPage: number,
totalPages: number,
@@ -454,15 +460,15 @@ class DashboardService {
* Supports priority and SLA filters for consistent stats behavior
*/
async getApproverPerformance(
- dateRange?: DateRange,
- page: number = 1,
- limit: number = 10,
- startDate?: Date,
+ dateRange?: DateRange,
+ page: number = 1,
+ limit: number = 10,
+ startDate?: Date,
endDate?: Date,
priority?: string,
slaCompliance?: string
- ): Promise<{
- performance: ApproverPerformance[],
+ ): Promise<{
+ performance: ApproverPerformance[],
pagination: {
currentPage: number,
totalPages: number,
@@ -471,9 +477,9 @@ class DashboardService {
}
}> {
try {
- const params: any = {
- dateRange,
- page,
+ const params: any = {
+ dateRange,
+ page,
limit: limit || 10 // Explicitly set limit (default 10 if not provided)
};
if (dateRange === 'custom' && startDate && endDate) {
@@ -486,9 +492,9 @@ class DashboardService {
if (slaCompliance && slaCompliance !== 'all') {
params.slaCompliance = slaCompliance;
}
-
+
console.log('[Dashboard Service] Fetching approver performance with params:', params);
-
+
const response = await apiClient.get('/dashboard/stats/approver-performance', { params });
return {
performance: response.data.data,
@@ -504,13 +510,13 @@ class DashboardService {
* Get Request Lifecycle Report
*/
async getLifecycleReport(
- page: number = 1,
+ page: number = 1,
limit: number = 50,
dateRange?: DateRange,
startDate?: Date,
endDate?: Date
- ): Promise<{
- lifecycleData: any[],
+ ): Promise<{
+ lifecycleData: any[],
pagination: {
currentPage: number,
totalPages: number,
@@ -540,7 +546,7 @@ class DashboardService {
* Get enhanced User Activity Log Report
*/
async getActivityLogReport(
- page: number = 1,
+ page: number = 1,
limit: number = 50,
dateRange?: DateRange,
filterUserId?: string,
@@ -549,8 +555,8 @@ class DashboardService {
filterSeverity?: string,
startDate?: Date,
endDate?: Date
- ): Promise<{
- activities: any[],
+ ): Promise<{
+ activities: any[],
pagination: {
currentPage: number,
totalPages: number,
@@ -599,8 +605,8 @@ class DashboardService {
dateRange?: DateRange,
startDate?: Date,
endDate?: Date
- ): Promise<{
- agingData: any[],
+ ): Promise<{
+ agingData: any[],
pagination: {
currentPage: number,
totalPages: number,
@@ -647,7 +653,7 @@ class DashboardService {
}
if (priority && priority !== 'all') params.priority = priority;
if (slaCompliance && slaCompliance !== 'all') params.slaCompliance = slaCompliance;
-
+
const response = await apiClient.get('/dashboard/stats/single-approver', { params });
return response.data.data;
} catch (error) {
@@ -670,8 +676,8 @@ class DashboardService {
priority?: string,
slaCompliance?: string,
search?: string
- ): Promise<{
- requests: any[],
+ ): Promise<{
+ requests: any[],
pagination: {
currentPage: number,
totalPages: number,
@@ -690,7 +696,7 @@ class DashboardService {
if (priority) params.priority = priority;
if (slaCompliance) params.slaCompliance = slaCompliance;
if (search) params.search = search;
-
+
const response = await apiClient.get('/dashboard/requests/by-approver', { params });
return {
requests: response.data.data,
diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts
index 54c391a..98064c5 100644
--- a/src/services/workflowApi.ts
+++ b/src/services/workflowApi.ts
@@ -120,11 +120,11 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
approvers: Array.from({ length: form.approverCount || 1 }, (_, i) => {
const a = form.approvers[i] || ({} as any);
const tat = typeof a.tat === 'number' ? a.tat : 0;
-
+
if (!a.email || !a.email.trim()) {
throw new Error(`Email is required for approver at level ${i + 1}.`);
}
-
+
return {
email: a.email,
tat: tat,
@@ -132,14 +132,14 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
};
}),
};
-
+
// Add spectators if any (simplified - only email required)
if (form.spectators && form.spectators.length > 0) {
payload.spectators = form.spectators
.filter((s: any) => s?.email)
.map((s: any) => ({ email: s.email }));
}
-
+
// Note: participants array is auto-generated by backend from approvers and spectators
// No need to build or send it from frontend
@@ -155,38 +155,39 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
return { id: data?.requestId } as any;
}
-export async function listWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
- const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params;
- const res = await apiClient.get('/workflows', {
- params: {
- page,
- limit,
- search,
- status,
- priority,
+export async function listWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; lifecycle?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
+ const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, lifecycle, dateRange, startDate, endDate } = params;
+ const res = await apiClient.get('/workflows', {
+ params: {
+ page,
+ limit,
+ search,
+ status,
+ priority,
templateType,
- department,
- initiator,
- approver,
- slaCompliance,
- dateRange,
- startDate,
- endDate
- }
+ department,
+ initiator,
+ approver,
+ slaCompliance,
+ lifecycle,
+ dateRange,
+ startDate,
+ endDate
+ }
});
return res.data?.data || res.data;
}
// List requests where user is a participant (not initiator) - for regular users' "All Requests" page
// SEPARATE from listWorkflows (admin) to avoid interference
-export async function listParticipantRequests(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
- const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate } = params;
- const res = await apiClient.get('/workflows/participant-requests', {
- params: {
- page,
- limit,
- search,
- status,
+export async function listParticipantRequests(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; initiator?: string; approver?: string; approverType?: string; slaCompliance?: string; lifecycle?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
+ const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, lifecycle, dateRange, startDate, endDate } = params;
+ const res = await apiClient.get('/workflows/participant-requests', {
+ params: {
+ page,
+ limit,
+ search,
+ status,
priority,
templateType,
department,
@@ -194,6 +195,7 @@ export async function listParticipantRequests(params: { page?: number; limit?: n
approver,
approverType,
slaCompliance,
+ lifecycle,
dateRange,
startDate,
endDate
@@ -210,12 +212,12 @@ export async function listParticipantRequests(params: { page?: number; limit?: n
// List requests where user is a participant (not initiator) - for "All Requests" page
export async function listMyWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; department?: string; initiator?: string; approver?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
const { page = 1, limit = 20, search, status, priority, department, initiator, approver, slaCompliance, dateRange, startDate, endDate } = params;
- const res = await apiClient.get('/workflows/my', {
- params: {
- page,
- limit,
- search,
- status,
+ const res = await apiClient.get('/workflows/my', {
+ params: {
+ page,
+ limit,
+ search,
+ status,
priority,
department,
initiator,
@@ -224,7 +226,7 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
dateRange,
startDate,
endDate
- }
+ }
});
// Response structure: { success, data: { data: [...], pagination: {...} } }
return {
@@ -234,22 +236,23 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
}
// List requests where user is the initiator - for "My Requests" page
-export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; slaCompliance?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
- const { page = 1, limit = 20, search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate } = params;
- const res = await apiClient.get('/workflows/my-initiated', {
- params: {
- page,
- limit,
- search,
- status,
+export async function listMyInitiatedWorkflows(params: { page?: number; limit?: number; search?: string; status?: string; priority?: string; templateType?: string; department?: string; slaCompliance?: string; lifecycle?: string; dateRange?: string; startDate?: string; endDate?: string } = {}) {
+ const { page = 1, limit = 20, search, status, priority, templateType, department, slaCompliance, lifecycle, dateRange, startDate, endDate } = params;
+ const res = await apiClient.get('/workflows/my-initiated', {
+ params: {
+ page,
+ limit,
+ search,
+ status,
priority,
templateType,
department,
slaCompliance,
+ lifecycle,
dateRange,
startDate,
endDate
- }
+ }
});
// Response structure: { success, data: { data: [...], pagination: {...} } }
return {
@@ -304,22 +307,22 @@ export async function addApprover(requestId: string, email: string) {
}
export async function addApproverAtLevel(
- requestId: string,
- email: string,
- tatHours: number,
+ requestId: string,
+ email: string,
+ tatHours: number,
level: number
) {
- const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, {
- email,
- tatHours,
- level
+ const res = await apiClient.post(`/workflows/${requestId}/approvers/at-level`, {
+ email,
+ tatHours,
+ level
});
return res.data?.data || res.data;
}
export async function skipApprover(requestId: string, levelId: string, reason?: string) {
- const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, {
- reason
+ const res = await apiClient.post(`/workflows/${requestId}/approvals/${levelId}/skip`, {
+ reason
});
return res.data?.data || res.data;
}
@@ -376,7 +379,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
if (!contentDisposition) {
return 'download';
}
-
+
// Try to extract from filename* first (RFC 5987 encoded) - preferred for non-ASCII
const filenameStarMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
if (filenameStarMatch && filenameStarMatch[1]) {
@@ -386,7 +389,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
// If decoding fails, fall back to regular filename
}
}
-
+
// Fallback to regular filename (for ASCII-only filenames)
const filenameMatch = contentDisposition.match(/filename="?([^";]+)"?/);
if (filenameMatch && filenameMatch.length > 1 && filenameMatch[1]) {
@@ -396,7 +399,7 @@ function extractFilenameFromContentDisposition(contentDisposition: string | null
const extracted = parts[0]?.trim();
return extracted || 'download';
}
-
+
return 'download';
}
@@ -404,34 +407,34 @@ export async function downloadDocument(documentId: string): Promise {
const baseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
const downloadUrl = `${baseURL}/api/v1/workflows/documents/${documentId}/download`;
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
-
+
try {
// Build fetch options
const fetchOptions: RequestInit = {
credentials: 'include', // Send cookies in production
};
-
+
// In development, add Authorization header from localStorage
if (!isProduction) {
const token = localStorage.getItem('accessToken');
fetchOptions.headers = {
'Authorization': `Bearer ${token}`
};
- }
-
+ }
+
const response = await fetch(downloadUrl, fetchOptions);
-
+
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Download failed: ${response.status} - ${errorText}`);
}
-
+
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
-
+
const contentDisposition = response.headers.get('Content-Disposition');
const filename = extractFilenameFromContentDisposition(contentDisposition);
-
+
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = filename;
@@ -449,35 +452,35 @@ export async function downloadWorkNoteAttachment(attachmentId: string): Promise<
const downloadBaseURL = import.meta.env.VITE_BASE_URL || 'http://localhost:5000';
const downloadUrl = `${downloadBaseURL}/api/v1/workflows/work-notes/attachments/${attachmentId}/download`;
const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production';
-
+
try {
// Build fetch options
const fetchOptions: RequestInit = {
credentials: 'include', // Send cookies in production
};
-
+
// In development, add Authorization header from localStorage
if (!isProduction) {
const token = localStorage.getItem('accessToken');
fetchOptions.headers = {
'Authorization': `Bearer ${token}`
};
- }
-
+ }
+
const response = await fetch(downloadUrl, fetchOptions);
-
+
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Download failed: ${response.status} - ${errorText}`);
}
-
+
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
-
+
// Get filename from Content-Disposition header or use default
const contentDisposition = response.headers.get('Content-Disposition');
const filename = extractFilenameFromContentDisposition(contentDisposition);
-
+
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = filename;
@@ -522,14 +525,14 @@ export async function updateWorkflowMultipart(requestId: string, updateData: any
...updateData,
deleteDocumentIds: deleteDocumentIds || []
};
-
+
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('category', 'SUPPORTING');
if (files && files.length > 0) {
files.forEach(f => formData.append('files', f));
}
-
+
const res = await apiClient.put(`/workflows/${requestId}/multipart`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
@@ -560,10 +563,10 @@ export async function updateAndSubmitWorkflow(requestId: string, workflowData: C
description: workflowData.description,
priority: workflowData.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD',
};
-
+
// Update workflow details
await apiClient.put(`/workflows/${requestId}`, payload);
-
+
// If files provided, update documents (this would need backend support for updating documents)
// For now, we'll just submit the updated workflow
const res = await apiClient.patch(`/workflows/${requestId}/submit`);
@@ -577,7 +580,7 @@ export async function updateBreachReason(levelId: string, breachReason: string):
const response = await apiClient.put(`/tat/breach-reason/${levelId}`, {
breachReason
});
-
+
if (!response.data.success) {
throw new Error(response.data.error || 'Failed to update breach reason');
}
diff --git a/src/utils/requestDetailHelpers.tsx b/src/utils/requestDetailHelpers.tsx
index 9eb26cb..54ab7d5 100644
--- a/src/utils/requestDetailHelpers.tsx
+++ b/src/utils/requestDetailHelpers.tsx
@@ -1,14 +1,14 @@
-import {
- CheckCircle,
- XCircle,
- Clock,
- MessageSquare,
- RefreshCw,
- UserPlus,
- FileText,
- Paperclip,
- AlertTriangle,
- Activity
+import {
+ CheckCircle,
+ XCircle,
+ Clock,
+ MessageSquare,
+ RefreshCw,
+ UserPlus,
+ FileText,
+ Paperclip,
+ AlertTriangle,
+ Activity
} from 'lucide-react';
/**
@@ -110,6 +110,39 @@ export const getStatusConfig = (status: string) => {
}
};
+/**
+ * Utility: getWorkflowStateConfig
+ *
+ * Purpose: Get display configuration for workflow lifecycle badges (Open, Closed, Draft)
+ *
+ * @param state - workflowState string from backend
+ * @returns Configuration object with Tailwind CSS classes
+ */
+export const getWorkflowStateConfig = (state: string) => {
+ switch (state?.toUpperCase()) {
+ case 'DRAFT':
+ return {
+ color: 'bg-gray-100 text-gray-800 border-gray-200',
+ label: 'draft'
+ };
+ case 'OPEN':
+ return {
+ color: 'bg-blue-100 text-blue-800 border-blue-200',
+ label: 'open'
+ };
+ case 'CLOSED':
+ return {
+ color: 'bg-slate-100 text-slate-800 border-slate-200',
+ label: 'closed'
+ };
+ default:
+ return {
+ color: 'bg-gray-100 text-gray-800 border-gray-200',
+ label: state?.toLowerCase() || 'open'
+ };
+ }
+};
+
/**
* Utility: getActionTypeIcon
*
diff --git a/src/utils/slaTracker.ts b/src/utils/slaTracker.ts
index d8bd862..9a3f433 100644
--- a/src/utils/slaTracker.ts
+++ b/src/utils/slaTracker.ts
@@ -16,7 +16,7 @@ let configLoaded = false;
// Lazy initialization of configuration
async function ensureConfigLoaded() {
if (configLoaded) return;
-
+
try {
const config = await configService.getConfig();
WORK_START_HOUR = config.workingHours.START_HOUR;
@@ -30,7 +30,7 @@ async function ensureConfigLoaded() {
}
// Initialize config on first import (non-blocking)
-ensureConfigLoaded().catch(() => {});
+ensureConfigLoaded().catch(() => { });
/**
* Check if current time is within working hours
@@ -40,7 +40,7 @@ ensureConfigLoaded().catch(() => {});
export function isWorkingTime(date: Date = new Date(), priority: string = 'standard'): boolean {
const day = date.getDay(); // 0 = Sunday, 6 = Saturday
const hour = date.getHours();
-
+
// For standard priority: exclude weekends
// For express priority: include weekends (calendar days)
if (priority === 'standard') {
@@ -48,14 +48,14 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
return false;
}
}
-
+
// Working hours check (applies to both priorities)
if (hour < WORK_START_HOUR || hour >= WORK_END_HOUR) {
return false;
}
-
+
// TODO: Add holiday check if holiday API is available
-
+
return true;
}
@@ -66,12 +66,12 @@ export function isWorkingTime(date: Date = new Date(), priority: string = 'stand
*/
export function getNextWorkingTime(date: Date = new Date(), priority: string = 'standard'): Date {
const result = new Date(date);
-
+
// If already in working time, return as is
if (isWorkingTime(result, priority)) {
return result;
}
-
+
// For standard priority: skip weekends
if (priority === 'standard') {
const day = result.getDay();
@@ -86,13 +86,13 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = '
return result;
}
}
-
+
// If before work hours, move to work start
if (result.getHours() < WORK_START_HOUR) {
result.setHours(WORK_START_HOUR, 0, 0, 0);
return result;
}
-
+
// If after work hours, move to next day work start
if (result.getHours() >= WORK_END_HOUR) {
result.setDate(result.getDate() + 1);
@@ -100,7 +100,7 @@ export function getNextWorkingTime(date: Date = new Date(), priority: string = '
// Check if next day is weekend (only for standard priority)
return getNextWorkingTime(result, priority);
}
-
+
return result;
}
@@ -114,19 +114,19 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne
let current = new Date(startDate);
const end = new Date(endDate);
let elapsedMinutes = 0;
-
+
// Move minute by minute and count only working minutes
while (current < end) {
if (isWorkingTime(current, priority)) {
elapsedMinutes++;
}
current.setMinutes(current.getMinutes() + 1);
-
+
// Safety: stop if calculating more than 1 year
const hoursSoFar = elapsedMinutes / 60;
if (hoursSoFar > 8760) break;
}
-
+
// Convert minutes to hours (with decimal precision)
return elapsedMinutes / 60;
}
@@ -140,12 +140,12 @@ export function calculateElapsedWorkingHours(startDate: Date, endDate: Date = ne
export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date = new Date(), priority: string = 'standard'): number {
const deadlineTime = new Date(deadline).getTime();
const currentTime = new Date(fromDate).getTime();
-
+
// If deadline has passed
if (deadlineTime <= currentTime) {
return 0;
}
-
+
// Calculate remaining working hours
return calculateElapsedWorkingHours(fromDate, deadline, priority);
}
@@ -160,9 +160,9 @@ export function calculateRemainingWorkingHours(deadline: Date, fromDate: Date =
export function calculateSLAProgress(startDate: Date, deadline: Date, currentDate: Date = new Date(), priority: string = 'standard'): number {
const totalHours = calculateElapsedWorkingHours(startDate, deadline, priority);
const elapsedHours = calculateElapsedWorkingHours(startDate, currentDate, priority);
-
+
if (totalHours === 0) return 0;
-
+
const progress = (elapsedHours / totalHours) * 100;
return Math.min(Math.max(progress, 0), 100); // Clamp between 0 and 100
}
@@ -185,17 +185,17 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
const start = new Date(startDate);
const end = new Date(deadline);
const now = new Date();
-
+
const isWorking = isWorkingTime(now, priority);
const elapsedHours = calculateElapsedWorkingHours(start, now, priority);
const totalHours = calculateElapsedWorkingHours(start, end, priority);
const remainingHours = Math.max(0, totalHours - elapsedHours);
const progress = calculateSLAProgress(start, end, now, priority);
-
+
let statusText = '';
if (!isWorking) {
- statusText = priority === 'express'
- ? 'SLA tracking paused (outside working hours)'
+ statusText = priority === 'express'
+ ? 'SLA tracking paused (outside working hours)'
: 'SLA tracking paused (outside working hours/days)';
} else if (remainingHours === 0) {
statusText = 'SLA deadline reached';
@@ -208,7 +208,7 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
} else {
statusText = 'On track';
}
-
+
return {
isWorkingTime: isWorking,
progress,
@@ -231,43 +231,31 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
export function formatHoursMinutes(hours: number | null | undefined): string {
if (hours === null || hours === undefined || hours < 0) return '0 hours';
if (hours === 0) return '0 hours';
-
+
const WORKING_HOURS_PER_DAY = 8;
-
+
// If less than 1 hour, show minutes only
if (hours < 1) {
const m = Math.round(hours * 60);
return m > 0 ? `${m}m` : '0 hours';
}
-
+
// Calculate days and remaining hours (8 hours = 1 day)
- // Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts
const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY);
const minutes = Math.round((hours % 1) * 60);
-
- // If we have days, format with days (matching backend format)
+
+ const dayLabel = days === 1 ? 'day' : 'days';
+
if (days > 0) {
- const dayLabel = days === 1 ? 'day' : 'days';
- const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
- const minuteLabel = minutes === 1 ? 'min' : 'm';
-
- if (minutes > 0) {
- return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
- } else {
- return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`;
- }
- }
-
- // No days, just hours and minutes
- const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
- const minuteLabel = minutes === 1 ? 'min' : 'm';
-
- if (minutes > 0) {
- return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
- } else {
- return `${remainingHrs} ${hourLabel}`;
+ return minutes > 0
+ ? `${days} ${dayLabel} ${remainingHrs}h ${minutes}min`
+ : `${days} ${dayLabel} ${remainingHrs}h`;
}
+
+ return minutes > 0
+ ? `${remainingHrs}h ${minutes}min`
+ : `${remainingHrs}h`;
}
/**
@@ -276,25 +264,25 @@ export function formatHoursMinutes(hours: number | null | undefined): string {
export function formatWorkingHours(hours: number): string {
if (hours === 0) return '0h';
if (hours < 0) return '0h';
-
+
const totalMinutes = Math.round(hours * 60);
const days = Math.floor(totalMinutes / (8 * 60)); // 8 working hours per day
const remainingMinutes = totalMinutes % (8 * 60);
const remainingHours = Math.floor(remainingMinutes / 60);
const minutes = remainingMinutes % 60;
-
+
if (days > 0 && remainingHours > 0 && minutes > 0) {
- return `${days}d ${remainingHours}h ${minutes}m`;
+ return `${days}d ${remainingHours}h ${minutes}min`;
} else if (days > 0 && remainingHours > 0) {
return `${days}d ${remainingHours}h`;
} else if (days > 0) {
return `${days}d`;
} else if (remainingHours > 0 && minutes > 0) {
- return `${remainingHours}h ${minutes}m`;
+ return `${remainingHours}h ${minutes}min`;
} else if (remainingHours > 0) {
return `${remainingHours}h`;
} else {
- return `${minutes}m`;
+ return `${minutes}min`;
}
}
@@ -306,14 +294,14 @@ export function getTimeUntilNextWorking(priority: string = 'standard'): string {
if (isWorkingTime(new Date(), priority)) {
return 'In working hours';
}
-
+
const now = new Date();
const next = getNextWorkingTime(now, priority);
const diff = next.getTime() - now.getTime();
-
+
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
-
+
if (hours > 24) {
const days = Math.floor(hours / 24);
return `Resumes in ${days}d ${hours % 24}h`;