front changes started after separating closed status for the status attribute based on this change alteration done on various filters

This commit is contained in:
laxmanhalaki 2026-02-04 20:25:03 +05:30
parent 1d205a4038
commit 6b4b80c0d4
42 changed files with 1454 additions and 1230 deletions

View File

@ -1,6 +1,7 @@
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Star } from 'lucide-react'; import { Star } from 'lucide-react';
import { formatBreachTime } from '@/pages/Dashboard/utils/dashboardCalculations';
export interface CriticalAlertData { export interface CriticalAlertData {
requestId: string; requestId: string;
@ -12,6 +13,8 @@ export interface CriticalAlertData {
breachCount: number; breachCount: number;
currentLevel: number; currentLevel: number;
totalLevels: number; totalLevels: number;
isActionable?: boolean;
requestRole?: 'APPROVER' | 'INITIATOR' | 'PARTICIPANT';
} }
interface CriticalAlertCardProps { interface CriticalAlertCardProps {
@ -40,23 +43,29 @@ const calculateProgress = (alert: CriticalAlertData) => {
return Math.min(100, Math.max(0, Math.round(percentageUsed))); return Math.min(100, Math.max(0, Math.round(percentageUsed)));
}; };
const formatRemainingTime = (alert: CriticalAlertData) => { const formatDisplayTime = (alert: CriticalAlertData) => {
if (alert.totalTATHours === undefined || alert.totalTATHours === null) return 'N/A'; if (alert.totalTATHours === undefined || alert.totalTATHours === null) return 'N/A';
const hours = alert.totalTATHours; const hours = alert.totalTATHours;
const isOverdue = hours <= 0;
const absHours = Math.abs(hours);
// If TAT is breached (negative or zero) const formattedTime = formatBreachTime(absHours);
if (hours <= 0) {
const overdue = Math.abs(hours); if (formattedTime === 'Just breached') return 'Breached';
if (overdue < 1) return `Breached`;
if (overdue < 24) return `${Math.round(overdue)}h overdue`; return isOverdue ? `${formattedTime} overdue` : `${formattedTime} left`;
return `${Math.round(overdue / 24)}d overdue`; };
const getRoleBadge = (role?: string) => {
switch (role) {
case 'APPROVER':
return { label: 'Action Required', className: 'bg-red-100 text-red-700 border-red-200' };
case 'INITIATOR':
return { label: 'My Request', className: 'bg-orange-100 text-orange-700 border-orange-200' };
default:
return { label: 'Monitoring', className: 'bg-blue-100 text-blue-700 border-blue-200' };
} }
// If TAT is still remaining
if (hours < 1) return `${Math.round(hours * 60)}min left`;
if (hours < 24) return `${Math.round(hours)}h left`;
return `${Math.round(hours / 24)}d left`;
}; };
export function CriticalAlertCard({ export function CriticalAlertCard({
@ -65,10 +74,15 @@ export function CriticalAlertCard({
testId = 'critical-alert-card' testId = 'critical-alert-card'
}: CriticalAlertCardProps) { }: CriticalAlertCardProps) {
const progress = calculateProgress(alert); const progress = calculateProgress(alert);
const isActionable = alert.isActionable ?? true; // Default to true if not provided (admin view)
const roleInfo = getRoleBadge(alert.requestRole);
return ( return (
<div <div
className="p-3 sm:p-4 bg-red-50 rounded-lg sm:rounded-xl border border-red-100 hover:shadow-md transition-all duration-200 cursor-pointer" className={`p-3 sm:p-4 rounded-lg sm:rounded-xl border hover:shadow-md transition-all duration-200 cursor-pointer ${isActionable
? 'bg-red-50 border-red-100'
: 'bg-orange-50/50 border-orange-100'
}`}
onClick={() => onNavigate?.(alert.requestNumber)} onClick={() => onNavigate?.(alert.requestNumber)}
data-testid={`${testId}-${alert.requestId}`} data-testid={`${testId}-${alert.requestId}`}
> >
@ -83,14 +97,22 @@ export function CriticalAlertCard({
</p> </p>
{alert.priority === 'express' && ( {alert.priority === 'express' && (
<Star <Star
className="h-3 w-3 text-red-500 flex-shrink-0" className={`h-3 w-3 flex-shrink-0 ${isActionable ? 'text-red-500' : 'text-orange-500'}`}
data-testid={`${testId}-priority-icon`} data-testid={`${testId}-priority-icon`}
/> />
)} )}
{alert.requestRole && (
<Badge
variant="outline"
className={`text-[10px] px-1.5 py-0 h-4 ${roleInfo.className}`}
>
{roleInfo.label}
</Badge>
)}
{alert.breachCount > 0 && ( {alert.breachCount > 0 && (
<Badge <Badge
variant="destructive" variant="destructive"
className="text-xs" className="text-[10px] px-1.5 py-0 h-4"
data-testid={`${testId}-breach-count`} data-testid={`${testId}-breach-count`}
> >
{alert.breachCount} {alert.breachCount}
@ -106,10 +128,11 @@ export function CriticalAlertCard({
</div> </div>
<Badge <Badge
variant="outline" variant="outline"
className="text-xs bg-white border-red-200 text-red-700 font-medium whitespace-nowrap" className={`text-xs bg-white font-medium whitespace-nowrap ${isActionable ? 'border-red-200 text-red-700' : 'border-orange-200 text-orange-700'
}`}
data-testid={`${testId}-remaining-time`} data-testid={`${testId}-remaining-time`}
> >
{formatRemainingTime(alert)} {formatDisplayTime(alert)}
</Badge> </Badge>
</div> </div>
<div className="space-y-1 sm:space-y-2"> <div className="space-y-1 sm:space-y-2">
@ -124,8 +147,7 @@ export function CriticalAlertCard({
</div> </div>
<Progress <Progress
value={progress} value={progress}
className={`h-1.5 sm:h-2 ${ className={`h-1.5 sm:h-2 ${progress >= 80 ? '[&>div]:bg-red-600' :
progress >= 80 ? '[&>div]:bg-red-600' :
progress >= 50 ? '[&>div]:bg-orange-500' : progress >= 50 ? '[&>div]:bg-orange-500' :
'[&>div]:bg-green-600' '[&>div]:bg-green-600'
}`} }`}

View File

@ -240,7 +240,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
id: m.noteId || m.id || String(Math.random()), id: m.noteId || m.id || String(Math.random()),
user: { user: {
name: m.userName || 'User', name: m.userName || 'User',
avatar: (m.userName || 'U').slice(0,2).toUpperCase(), avatar: (m.userName || 'U').slice(0, 2).toUpperCase(),
role: m.userRole || 'Participant' role: m.userRole || 'Participant'
}, },
content: m.message || '', content: m.message || '',
@ -361,7 +361,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
const userId = p.userId || p.user_id || ''; const userId = p.userId || p.user_id || '';
return { return {
name: p.userName || p.user_name || p.user_email || p.userEmail || 'User', name: p.userName || p.user_name || p.user_email || p.userEmail || 'User',
avatar: (p.userName || p.user_name || p.user_email || 'U').toString().split(' ').map((s: string)=>s[0]).filter(Boolean).join('').slice(0,2).toUpperCase(), avatar: (p.userName || p.user_name || p.user_email || 'U').toString().split(' ').map((s: string) => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase(),
role: formatParticipantRole(participantType.toString()), role: formatParticipantRole(participantType.toString()),
status: 'offline', // will be updated by presence events status: 'offline', // will be updated by presence events
email: p.userEmail || p.user_email || '', email: p.userEmail || p.user_email || '',
@ -464,7 +464,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
if (details?.workflow?.requestId) { if (details?.workflow?.requestId) {
joinedId = details.workflow.requestId; // join by UUID to match server emits joinedId = details.workflow.requestId; // join by UUID to match server emits
} }
} catch {} } catch { }
try { try {
// Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL) // Get socket using helper function (handles VITE_BASE_URL or VITE_API_BASE_URL)
const s = getSocket(); // Uses getSocketBaseUrl() helper internally const s = getSocket(); // Uses getSocketBaseUrl() helper internally
@ -693,10 +693,10 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
// Socket cleanup completed - logging removed // Socket cleanup completed - logging removed
}; };
(window as any).__wn_cleanup = cleanup; (window as any).__wn_cleanup = cleanup;
} catch {} } catch { }
})(); })();
return () => { return () => {
try { (window as any).__wn_cleanup?.(); } catch {} try { (window as any).__wn_cleanup?.(); } catch { }
}; };
}, [effectiveRequestId, currentUserId, skipSocketJoin]); }, [effectiveRequestId, currentUserId, skipSocketJoin]);
@ -762,7 +762,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
id: m.noteId || m.id || String(Math.random()), id: m.noteId || m.id || String(Math.random()),
user: { user: {
name: m.userName || 'User', name: m.userName || 'User',
avatar: (m.userName || 'U').slice(0,2).toUpperCase(), avatar: (m.userName || 'U').slice(0, 2).toUpperCase(),
role: m.userRole || 'Participant' role: m.userRole || 'Participant'
}, },
content: m.message || '', content: m.message || '',
@ -779,8 +779,18 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
})) : undefined })) : undefined
}; };
}) : []; }) : [];
setMessages(mapped as any); setMessages(prev => {
} catch { // Keep system messages (activities) from the previous state
const systemMessages = prev.filter(m => m.isSystem);
// Combine with the newly fetched work notes
const combined = [...mapped, ...systemMessages];
// Sort to maintain chronological order
return combined.sort((a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
) as any;
});
} catch (error) {
console.error('[WorkNoteChat] Failed to send message or fetch notes:', error);
setMessages(prev => [...prev, newMessage]); setMessages(prev => [...prev, newMessage]);
} }
} }
@ -1274,8 +1284,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
<div key={msg.id} className={`flex gap-2 sm:gap-3 lg:gap-4 ${msg.isSystem ? 'justify-center' : isCurrentUser ? 'justify-end' : ''}`}> <div key={msg.id} className={`flex gap-2 sm:gap-3 lg:gap-4 ${msg.isSystem ? 'justify-center' : isCurrentUser ? 'justify-end' : ''}`}>
{!msg.isSystem && !isCurrentUser && ( {!msg.isSystem && !isCurrentUser && (
<Avatar className="h-8 w-8 sm:h-10 sm:w-10 lg:h-12 lg:w-12 flex-shrink-0 ring-1 sm:ring-2 ring-white shadow-sm"> <Avatar className="h-8 w-8 sm:h-10 sm:w-10 lg:h-12 lg:w-12 flex-shrink-0 ring-1 sm:ring-2 ring-white shadow-sm">
<AvatarFallback className={`text-white font-semibold text-xs sm:text-sm ${ <AvatarFallback className={`text-white font-semibold text-xs sm:text-sm ${msg.user.role === 'Initiator' ? 'bg-green-600' :
msg.user.role === 'Initiator' ? 'bg-green-600' :
msg.user.role === 'Current User' ? 'bg-blue-500' : msg.user.role === 'Current User' ? 'bg-blue-500' :
msg.user.role === 'System' ? 'bg-gray-500' : msg.user.role === 'System' ? 'bg-gray-500' :
'bg-slate-600' 'bg-slate-600'
@ -1414,8 +1423,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
<button <button
key={index} key={index}
onClick={() => addReaction(msg.id, reaction.emoji)} onClick={() => addReaction(msg.id, reaction.emoji)}
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs sm:text-sm transition-colors flex-shrink-0 ${ className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs sm:text-sm transition-colors flex-shrink-0 ${reaction.users.includes('You')
reaction.users.includes('You')
? 'bg-blue-100 text-blue-800 border border-blue-200' ? 'bg-blue-100 text-blue-800 border border-blue-200'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`} }`}
@ -1564,8 +1572,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
className="w-full flex items-center gap-3 p-3 hover:bg-blue-50 rounded-lg text-left transition-colors border border-transparent hover:border-blue-200" className="w-full flex items-center gap-3 p-3 hover:bg-blue-50 rounded-lg text-left transition-colors border border-transparent hover:border-blue-200"
> >
<Avatar className="h-10 w-10"> <Avatar className="h-10 w-10">
<AvatarFallback className={`text-white text-sm font-semibold ${ <AvatarFallback className={`text-white text-sm font-semibold ${participant.role === 'Initiator' ? 'bg-green-600' :
participant.role === 'Initiator' ? 'bg-green-600' :
participant.role === 'Approver' ? 'bg-purple-600' : participant.role === 'Approver' ? 'bg-purple-600' :
'bg-blue-500' 'bg-blue-500'
}`}> }`}>
@ -1713,8 +1720,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
<div key={index} className="flex items-center gap-3"> <div key={index} className="flex items-center gap-3">
<div className="relative"> <div className="relative">
<Avatar className="h-9 w-9 sm:h-10 sm:w-10"> <Avatar className="h-9 w-9 sm:h-10 sm:w-10">
<AvatarFallback className={`text-white font-semibold text-sm ${ <AvatarFallback className={`text-white font-semibold text-sm ${participant.role === 'Initiator' ? 'bg-green-600' :
participant.role === 'Initiator' ? 'bg-green-600' :
isCurrentUser ? 'bg-blue-500' : 'bg-slate-600' isCurrentUser ? 'bg-blue-500' : 'bg-slate-600'
}`}> }`}>
{participant.avatar} {participant.avatar}

View File

@ -22,6 +22,7 @@ import { CustomDatePicker } from '@/components/ui/date-picker';
interface StandardUserAllRequestsFiltersProps { interface StandardUserAllRequestsFiltersProps {
// Filters // Filters
searchTerm: string; searchTerm: string;
lifecycleFilter: string;
statusFilter: string; statusFilter: string;
priorityFilter: string; priorityFilter: string;
templateTypeFilter: string; templateTypeFilter: string;
@ -64,6 +65,7 @@ interface StandardUserAllRequestsFiltersProps {
// Actions // Actions
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onLifecycleChange: (value: string) => void;
onStatusChange: (value: string) => void; onStatusChange: (value: string) => void;
onPriorityChange: (value: string) => void; onPriorityChange: (value: string) => void;
onTemplateTypeChange: (value: string) => void; onTemplateTypeChange: (value: string) => void;
@ -85,6 +87,7 @@ interface StandardUserAllRequestsFiltersProps {
export function StandardUserAllRequestsFilters({ export function StandardUserAllRequestsFilters({
searchTerm, searchTerm,
lifecycleFilter,
statusFilter, statusFilter,
priorityFilter, priorityFilter,
// templateTypeFilter, // templateTypeFilter,
@ -102,6 +105,7 @@ export function StandardUserAllRequestsFilters({
initiatorSearch, initiatorSearch,
approverSearch, approverSearch,
onSearchChange, onSearchChange,
onLifecycleChange,
onStatusChange, onStatusChange,
onPriorityChange, onPriorityChange,
// onTemplateTypeChange, // onTemplateTypeChange,
@ -155,6 +159,17 @@ export function StandardUserAllRequestsFilters({
/> />
</div> </div>
<Select value={lifecycleFilter} onValueChange={onLifecycleChange}>
<SelectTrigger className="h-10" data-testid="lifecycle-filter">
<SelectValue placeholder="All Requests" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Requests</SelectItem>
<SelectItem value="open">Open Requests</SelectItem>
<SelectItem value="closed">Closed Requests</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusChange}> <Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger className="h-10" data-testid="status-filter"> <SelectTrigger className="h-10" data-testid="status-filter">
<SelectValue placeholder="All Status" /> <SelectValue placeholder="All Status" />

View File

@ -280,8 +280,9 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
setShowShareSummaryModal(true); setShowShareSummaryModal(true);
}; };
const needsClosure = (request?.status === 'approved' || request?.status === 'rejected') && isInitiator; const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
const isClosed = request?.status === 'closed'; const isClosed = apiRequest?.workflowState === 'CLOSED' || requestStatus === 'closed';
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator && !isClosed;
// Fetch summary details if request is closed // Fetch summary details if request is closed
useEffect(() => { useEffect(() => {
@ -419,7 +420,7 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
refreshing={refreshing} refreshing={refreshing}
onBack={onBack || (() => window.history.back())} onBack={onBack || (() => window.history.back())}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onShareSummary={handleShareSummary} onShareSummary={summaryId ? handleShareSummary : undefined}
isInitiator={isInitiator} isInitiator={isInitiator}
// Custom module: Business logic for preparing SLA data // Custom module: Business logic for preparing SLA data
slaData={request?.summary?.sla || request?.sla || null} slaData={request?.summary?.sla || request?.sla || null}
@ -516,6 +517,7 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
generationAttempts={generationAttempts} generationAttempts={generationAttempts}
generationFailed={generationFailed} generationFailed={generationFailed}
maxAttemptsReached={maxAttemptsReached} maxAttemptsReached={maxAttemptsReached}
isClosed={isClosed}
/> />
</TabsContent> </TabsContent>

View File

@ -44,6 +44,7 @@ interface ClaimManagementOverviewTabProps {
generationAttempts?: number; generationAttempts?: number;
generationFailed?: boolean; generationFailed?: boolean;
maxAttemptsReached?: boolean; maxAttemptsReached?: boolean;
isClosed?: boolean;
} }
export function ClaimManagementOverviewTab({ export function ClaimManagementOverviewTab({
@ -64,6 +65,7 @@ export function ClaimManagementOverviewTab({
generationAttempts = 0, generationAttempts = 0,
generationFailed = false, generationFailed = false,
maxAttemptsReached = false, maxAttemptsReached = false,
isClosed = false,
}: ClaimManagementOverviewTabProps) { }: ClaimManagementOverviewTabProps) {
// Check if this is a claim management request // Check if this is a claim management request
if (!isClaimManagementRequest(apiRequest)) { if (!isClaimManagementRequest(apiRequest)) {
@ -136,7 +138,7 @@ export function ClaimManagementOverviewTab({
<RequestInitiatorCard initiatorInfo={initiatorInfo} /> <RequestInitiatorCard initiatorInfo={initiatorInfo} />
{/* Closed Request Conclusion Remark Display */} {/* Closed Request Conclusion Remark Display */}
{apiRequest?.status === 'closed' && apiRequest?.conclusionRemark && ( {isClosed && apiRequest?.conclusionRemark && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
@ -166,18 +168,15 @@ export function ClaimManagementOverviewTab({
{/* Conclusion Remark Section - Closure Setup */} {/* Conclusion Remark Section - Closure Setup */}
{needsClosure && ( {needsClosure && (
<Card data-testid="conclusion-remark-card"> <Card data-testid="conclusion-remark-card">
<CardHeader className={`bg-gradient-to-r border-b ${ <CardHeader className={`bg-gradient-to-r border-b ${(apiRequest?.status || '').toLowerCase() === 'rejected'
(apiRequest?.status || '').toLowerCase() === 'rejected'
? 'from-red-50 to-rose-50 border-red-200' ? 'from-red-50 to-rose-50 border-red-200'
: 'from-green-50 to-emerald-50 border-green-200' : 'from-green-50 to-emerald-50 border-green-200'
}`}> }`}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div> <div>
<CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${ <CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-700' : 'text-green-700'
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-700' : 'text-green-700'
}`}> }`}>
<CheckCircle className={`w-5 h-5 ${ <CheckCircle className={`w-5 h-5 ${(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-600' : 'text-green-600'
(apiRequest?.status || '').toLowerCase() === 'rejected' ? 'text-red-600' : 'text-green-600'
}`} /> }`} />
Conclusion Remark - Final Step Conclusion Remark - Final Step
</CardTitle> </CardTitle>

View File

@ -219,7 +219,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
// Closure functionality - only for initiator when request is approved/rejected // Closure functionality - only for initiator when request is approved/rejected
// Check both lowercase and uppercase status values // Check both lowercase and uppercase status values
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase(); const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator; const isClosed = apiRequest?.workflowState === 'CLOSED' || requestStatus === 'closed';
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator && !isClosed;
// Closure check completed // Closure check completed
const { const {
@ -321,7 +322,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
setShowShareSummaryModal(true); setShowShareSummaryModal(true);
}; };
const isClosed = request?.status === 'closed'; // Summary check already handled by isClosed above
// Fetch summary details if request is closed // Fetch summary details if request is closed
useEffect(() => { useEffect(() => {
@ -490,7 +491,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
refreshing={refreshing} refreshing={refreshing}
onBack={onBack || (() => window.history.back())} onBack={onBack || (() => window.history.back())}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onShareSummary={handleShareSummary} onShareSummary={summaryId ? handleShareSummary : undefined}
isInitiator={isInitiator} isInitiator={isInitiator}
// Dealer-claim module: Business logic for preparing SLA data // Dealer-claim module: Business logic for preparing SLA data
slaData={request?.summary?.sla || request?.sla || null} slaData={request?.summary?.sla || request?.sla || null}
@ -593,6 +594,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
generationAttempts={generationAttempts} generationAttempts={generationAttempts}
generationFailed={generationFailed} generationFailed={generationFailed}
maxAttemptsReached={maxAttemptsReached} maxAttemptsReached={maxAttemptsReached}
isClosed={isClosed}
/> />
</TabsContent> </TabsContent>

View File

@ -294,6 +294,7 @@ export function useRequestDetails(
title: wf.title, title: wf.title,
description: wf.description, description: wf.description,
status: statusMap(wf.status), status: statusMap(wf.status),
workflowState: wf.workflowState,
priority: (wf.priority || '').toString().toLowerCase(), priority: (wf.priority || '').toString().toLowerCase(),
workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'), workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'),
approvalFlow, approvalFlow,
@ -564,6 +565,7 @@ export function useRequestDetails(
description: wf.description, description: wf.description,
priority, priority,
status: statusMap(wf.status), status: statusMap(wf.status),
workflowState: wf.workflowState,
workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'), workflowType: wf.workflowType || (wf.templateType === 'claim-management' ? 'CLAIM_MANAGEMENT' : 'NON_TEMPLATIZED'),
summary, summary,
initiator: { initiator: {

View File

@ -8,7 +8,7 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowRight, Calendar, CheckCircle } from 'lucide-react'; import { ArrowRight, Calendar, CheckCircle } from 'lucide-react';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { ClosedRequest } from '../types/closedRequests.types'; import { ClosedRequest } from '../types/closedRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig, getWorkflowStateConfig } from '../utils/configMappers';
interface ClosedRequestCardProps { interface ClosedRequestCardProps {
request: ClosedRequest; request: ClosedRequest;
@ -18,6 +18,7 @@ interface ClosedRequestCardProps {
export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardProps) { export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardProps) {
const priorityConfig = getPriorityConfig(request.priority); const priorityConfig = getPriorityConfig(request.priority);
const statusConfig = getStatusConfig(request.status); const statusConfig = getStatusConfig(request.status);
const stateConfig = getWorkflowStateConfig(request.workflowState || 'CLOSED');
const PriorityIcon = priorityConfig.icon; const PriorityIcon = priorityConfig.icon;
const StatusIcon = statusConfig.icon; const StatusIcon = statusConfig.icon;
@ -50,6 +51,12 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
<StatusIcon className="w-3.5 h-3.5 mr-1" /> <StatusIcon className="w-3.5 h-3.5 mr-1" />
{statusConfig.label} {statusConfig.label}
</Badge> </Badge>
<Badge
variant="outline"
className={`${stateConfig.color} text-xs px-2.5 py-0.5 font-semibold shrink-0`}
>
{stateConfig.label}
</Badge>
{request.department && ( {request.department && (
<Badge variant="secondary" className="bg-blue-50 text-blue-700 text-xs px-2.5 py-0.5 hidden sm:inline-flex"> <Badge variant="secondary" className="bg-blue-50 text-blue-700 text-xs px-2.5 py-0.5 hidden sm:inline-flex">
{request.department} {request.department}

View File

@ -8,7 +8,7 @@ export interface ClosedRequest {
displayId?: string; displayId?: string;
title: string; title: string;
description: string; description: string;
status: 'rejected' | 'closed'; status: 'rejected' | 'closed' | 'approved';
priority: 'express' | 'standard'; priority: 'express' | 'standard';
initiator: { name: string; avatar: string }; initiator: { name: string; avatar: string };
createdAt: string; createdAt: string;
@ -18,6 +18,7 @@ export interface ClosedRequest {
totalLevels?: number; totalLevels?: number;
completedLevels?: number; completedLevels?: number;
templateType?: string; // Template type for badge display templateType?: string; // Template type for badge display
workflowState?: string;
} }
export interface ClosedRequestsProps { export interface ClosedRequestsProps {

View File

@ -38,6 +38,14 @@ export function getStatusConfig(status: string): StatusConfig {
label: 'Closed', label: 'Closed',
description: 'Request finalized and archived' description: 'Request finalized and archived'
}; };
case 'approved':
return {
color: 'bg-green-100 text-green-800 border-green-200',
icon: CheckCircle,
iconColor: 'text-green-600',
label: 'Approved',
description: 'Request was approved'
};
case 'rejected': case 'rejected':
return { return {
color: 'bg-red-100 text-red-800 border-red-300', color: 'bg-red-100 text-red-800 border-red-300',
@ -57,3 +65,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'
};
}
}

View File

@ -11,7 +11,7 @@ export function transformClosedRequest(r: any): ClosedRequest {
displayId: r.requestNumber || r.request_number || r.requestId, displayId: r.requestNumber || r.request_number || r.requestId,
title: r.title, title: r.title,
description: r.description, description: r.description,
status: (r.status || '').toString().toLowerCase() as 'rejected' | 'closed', status: (r.status || '').toString().toLowerCase() as 'rejected' | 'closed' | 'approved',
priority: (r.priority || '').toString().toLowerCase() as 'express' | 'standard', priority: (r.priority || '').toString().toLowerCase() as 'express' | 'standard',
initiator: { initiator: {
name: r.initiator?.displayName || r.initiator?.email || '—', name: r.initiator?.displayName || r.initiator?.email || '—',
@ -29,6 +29,7 @@ export function transformClosedRequest(r: any): ClosedRequest {
totalLevels: r.totalLevels || 0, totalLevels: r.totalLevels || 0,
completedLevels: r.summary?.approvedLevels || 0, completedLevels: r.summary?.approvedLevels || 0,
templateType: r.templateType || r.template_type, // Template type for badge display templateType: r.templateType || r.template_type, // Template type for badge display
workflowState: r.workflowState || r.workflow_state,
}; };
} }

View File

@ -71,8 +71,8 @@ export function AdminKPICards({
}} }}
/> />
</div> </div>
{/* Row 2: Pending and Closed */} {/* Row 2: Pending and Paused */}
<div className="grid grid-cols-2 gap-2 mb-2"> <div className="grid grid-cols-2 gap-2">
<StatCard <StatCard
label="Pending" label="Pending"
value={kpis?.requestVolume.openRequests || 0} value={kpis?.requestVolume.openRequests || 0}
@ -84,21 +84,7 @@ export function AdminKPICards({
onKPIClick({ ...getFilterParams(), status: 'pending' }); onKPIClick({ ...getFilterParams(), status: 'pending' });
}} }}
/> />
<StatCard
label="Closed"
value={kpis?.requestVolume.closedRequests || 0}
bgColor="bg-gray-50"
textColor="text-gray-600"
testId="stat-closed"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), status: 'closed' });
}}
/>
</div>
{/* Row 3: Paused (if available) */}
{kpis?.requestVolume.pausedRequests !== undefined && ( {kpis?.requestVolume.pausedRequests !== undefined && (
<div className="grid grid-cols-2 gap-2">
<StatCard <StatCard
label="Paused" label="Paused"
value={kpis.requestVolume.pausedRequests || 0} value={kpis.requestVolume.pausedRequests || 0}
@ -110,8 +96,8 @@ export function AdminKPICards({
onKPIClick({ ...getFilterParams(), status: 'paused' }); onKPIClick({ ...getFilterParams(), status: 'paused' });
}} }}
/> />
</div>
)} )}
</div>
</KPICard> </KPICard>
{/* SLA Compliance */} {/* SLA Compliance */}

View File

@ -9,7 +9,7 @@ import { Progress } from '@/components/ui/progress';
import { Calendar as CalendarIcon } from 'lucide-react'; import { Calendar as CalendarIcon } from 'lucide-react';
import { UpcomingDeadline } from '@/services/dashboard.service'; import { UpcomingDeadline } from '@/services/dashboard.service';
import { Pagination } from '@/components/common/Pagination'; import { Pagination } from '@/components/common/Pagination';
import { formatHoursMinutes } from '@/utils/slaTracker'; import { formatBreachTime } from '../../utils/dashboardCalculations';
interface UpcomingDeadlinesSectionProps { interface UpcomingDeadlinesSectionProps {
isAdmin: boolean; isAdmin: boolean;
@ -67,8 +67,7 @@ export function UpcomingDeadlinesSection({
<span className="font-semibold text-xs sm:text-sm">{deadline.requestNumber}</span> <span className="font-semibold text-xs sm:text-sm">{deadline.requestNumber}</span>
<Badge <Badge
variant="outline" variant="outline"
className={`text-xs ${ className={`text-xs ${deadline.priority === 'express' ? 'bg-orange-50 text-orange-700' : 'bg-blue-50 text-blue-700'
deadline.priority === 'express' ? 'bg-orange-50 text-orange-700' : 'bg-blue-50 text-blue-700'
}`} }`}
> >
{deadline.priority} {deadline.priority}
@ -85,8 +84,7 @@ export function UpcomingDeadlinesSection({
<div className="text-right flex-shrink-0"> <div className="text-right flex-shrink-0">
<p className="text-xs text-muted-foreground">TAT Used</p> <p className="text-xs text-muted-foreground">TAT Used</p>
<p <p
className={`text-base sm:text-lg font-bold ${ className={`text-base sm:text-lg font-bold ${tatPercentage >= 80 ? 'text-red-600' : tatPercentage >= 50 ? 'text-orange-600' : 'text-green-600'
tatPercentage >= 80 ? 'text-red-600' : tatPercentage >= 50 ? 'text-orange-600' : 'text-green-600'
}`} }`}
> >
{tatPercentage.toFixed(0)}% {tatPercentage.toFixed(0)}%
@ -96,13 +94,12 @@ export function UpcomingDeadlinesSection({
<div className="space-y-1"> <div className="space-y-1">
<Progress <Progress
value={tatPercentage} value={tatPercentage}
className={`h-1.5 sm:h-2 ${ className={`h-1.5 sm:h-2 ${tatPercentage >= 80 ? 'bg-red-100' : tatPercentage >= 50 ? 'bg-orange-100' : 'bg-green-100'
tatPercentage >= 80 ? 'bg-red-100' : tatPercentage >= 50 ? 'bg-orange-100' : 'bg-green-100'
}`} }`}
/> />
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span>{formatHoursMinutes(elapsedHours)} elapsed</span> <span>{formatBreachTime(elapsedHours)} elapsed</span>
<span>{formatHoursMinutes(remainingHours)} left</span> <span>{formatBreachTime(Math.abs(remainingHours))} {remainingHours < 0 ? 'overdue' : 'left'}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -70,7 +70,7 @@ export function UserKPICards({
testId="kpi-my-requests" testId="kpi-my-requests"
onClick={() => onKPIClick(getFilterParams())} onClick={() => onKPIClick(getFilterParams())}
> >
<div className="grid grid-cols-3 gap-1.5 sm:gap-2"> <div className="grid grid-cols-2 gap-1.5 sm:gap-2">
<StatCard <StatCard
label="Approved" label="Approved"
value={kpis?.requestVolume.approvedRequests || 0} value={kpis?.requestVolume.approvedRequests || 0}
@ -115,17 +115,6 @@ export function UserKPICards({
onKPIClick({ ...getFilterParams(), status: 'rejected' }); onKPIClick({ ...getFilterParams(), status: 'rejected' });
}} }}
/> />
<StatCard
label="Closed"
value={kpis?.requestVolume.closedRequests || 0}
bgColor="bg-blue-50"
textColor="text-blue-600"
testId="stat-user-closed"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), status: 'closed' });
}}
/>
</div> </div>
</KPICard> </KPICard>

View File

@ -38,6 +38,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
statusFilter: filters.statusFilter, statusFilter: filters.statusFilter,
priorityFilter: filters.priorityFilter, priorityFilter: filters.priorityFilter,
templateTypeFilter: filters.templateTypeFilter, templateTypeFilter: filters.templateTypeFilter,
lifecycleFilter: filters.lifecycleFilter,
}); });
const hasInitialFetchRun = useRef(false); const hasInitialFetchRun = useRef(false);
@ -49,6 +50,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
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,
lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
}); });
hasInitialFetchRun.current = true; hasInitialFetchRun.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -63,7 +65,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
prev.searchTerm !== filters.searchTerm || prev.searchTerm !== filters.searchTerm ||
prev.statusFilter !== filters.statusFilter || prev.statusFilter !== filters.statusFilter ||
prev.priorityFilter !== filters.priorityFilter || prev.priorityFilter !== filters.priorityFilter ||
prev.templateTypeFilter !== filters.templateTypeFilter; prev.templateTypeFilter !== filters.templateTypeFilter ||
prev.lifecycleFilter !== filters.lifecycleFilter;
if (!hasChanged) return; // No actual change, skip if (!hasChanged) return; // No actual change, skip
@ -75,6 +78,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
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,
lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
}); });
// Update previous values // Update previous values
@ -83,12 +87,13 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
statusFilter: filters.statusFilter, statusFilter: filters.statusFilter,
priorityFilter: filters.priorityFilter, priorityFilter: filters.priorityFilter,
templateTypeFilter: filters.templateTypeFilter, templateTypeFilter: filters.templateTypeFilter,
lifecycleFilter: filters.lifecycleFilter,
}; };
}, filters.searchTerm !== prev.searchTerm ? 500 : 0); }, filters.searchTerm !== prev.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps // 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) // State for backend stats (calculated from entire dataset via SQL queries)
const [backendStats, setBackendStats] = useState<{ const [backendStats, setBackendStats] = useState<{
@ -131,7 +136,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
undefined, // approverType undefined, // approverType
filters.searchTerm || undefined, filters.searchTerm || undefined,
undefined, // slaCompliance 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({ setBackendStats({
@ -149,7 +155,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
} finally { } finally {
setLoadingStats(false); 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) // Fetch stats when filters change (excluding status filter)
// Stats should reflect priority and search filters, but NOT 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); }, filters.searchTerm ? 500 : 0);
return () => clearTimeout(timeoutId); 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) // Handle dynamic requests (fallback until API loads)
const convertedDynamicRequests = transformRequests(dynamicRequests); const convertedDynamicRequests = transformRequests(dynamicRequests);
@ -204,6 +210,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined, status: filters.statusFilter !== 'all' ? filters.statusFilter : undefined,
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,
lifecycle: filters.lifecycleFilter !== 'all' ? filters.lifecycleFilter : undefined,
}); });
} }
}, },
@ -243,6 +250,8 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
onStatusChange={filters.setStatusFilter} onStatusChange={filters.setStatusFilter}
onPriorityChange={filters.setPriorityFilter} onPriorityChange={filters.setPriorityFilter}
onTemplateTypeChange={filters.setTemplateTypeFilter} onTemplateTypeChange={filters.setTemplateTypeFilter}
lifecycleFilter={filters.lifecycleFilter}
onLifecycleChange={filters.setLifecycleFilter}
/> />
{/* Requests List */} {/* Requests List */}

View File

@ -12,10 +12,12 @@ interface MyRequestsFiltersProps {
statusFilter: string; statusFilter: string;
priorityFilter: string; priorityFilter: string;
templateTypeFilter: string; templateTypeFilter: string;
lifecycleFilter: string;
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onStatusChange: (value: string) => void; onStatusChange: (value: string) => void;
onPriorityChange: (value: string) => void; onPriorityChange: (value: string) => void;
onTemplateTypeChange: (value: string) => void; onTemplateTypeChange: (value: string) => void;
onLifecycleChange: (value: string) => void;
} }
export function MyRequestsFilters({ export function MyRequestsFilters({
@ -23,10 +25,12 @@ export function MyRequestsFilters({
statusFilter, statusFilter,
priorityFilter, priorityFilter,
// templateTypeFilter, // templateTypeFilter,
lifecycleFilter, // Destructure new prop
onSearchChange, onSearchChange,
onStatusChange, onStatusChange,
onPriorityChange, onPriorityChange,
// onTemplateTypeChange, // onTemplateTypeChange,
onLifecycleChange, // Destructure new prop
}: MyRequestsFiltersProps) { }: MyRequestsFiltersProps) {
return ( return (
<Card className="border-gray-200" data-testid="my-requests-filters"> <Card className="border-gray-200" data-testid="my-requests-filters">
@ -44,6 +48,21 @@ export function MyRequestsFilters({
</div> </div>
<div className="flex gap-2 sm:gap-3 w-full md:w-auto"> <div className="flex gap-2 sm:gap-3 w-full md:w-auto">
{/* Lifecycle Filter */}
<Select value={lifecycleFilter} onValueChange={onLifecycleChange}>
<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"
data-testid="lifecycle-filter"
>
<SelectValue placeholder="Lifecycle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Requests</SelectItem>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={onStatusChange}> <Select value={statusFilter} onValueChange={onStatusChange}>
<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"
@ -58,7 +77,6 @@ export function MyRequestsFilters({
<SelectItem value="paused">Paused</SelectItem> <SelectItem value="paused">Paused</SelectItem>
<SelectItem value="approved">Approved</SelectItem> <SelectItem value="approved">Approved</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem> <SelectItem value="rejected">Rejected</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -2,7 +2,7 @@
* My Requests Stats Section Component * 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 { StatsCard } from '@/components/dashboard/StatsCard';
import { MyRequestsStats } from '../types/myRequests.types'; import { MyRequestsStats } from '../types/myRequests.types';
@ -18,7 +18,7 @@ export function MyRequestsStatsSection({ stats, onStatusFilter }: MyRequestsStat
} }
}; };
return ( return (
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3 sm:gap-4" data-testid="my-requests-stats"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4" data-testid="my-requests-stats">
<StatsCard <StatsCard
label="Total" label="Total"
value={stats.total} value={stats.total}
@ -90,18 +90,6 @@ export function MyRequestsStatsSection({ stats, onStatusFilter }: MyRequestsStat
testId="stat-draft" testId="stat-draft"
onClick={onStatusFilter ? () => handleCardClick('draft') : undefined} onClick={onStatusFilter ? () => handleCardClick('draft') : undefined}
/> />
<StatsCard
label="Closed"
value={stats.closed}
icon={Archive}
iconColor="text-purple-600"
gradient="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200"
textColor="text-purple-700"
valueColor="text-purple-900"
testId="stat-closed"
onClick={onStatusFilter ? () => handleCardClick('closed') : undefined}
/>
</div> </div>
); );
} }

View File

@ -8,7 +8,7 @@ import { Badge } from '@/components/ui/badge';
import { ArrowRight, User, TrendingUp, Clock, Pause } from 'lucide-react'; import { ArrowRight, User, TrendingUp, Clock, Pause } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { MyRequest } from '../types/myRequests.types'; import { MyRequest } from '../types/myRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig, getWorkflowStateConfig } from '../utils/configMappers';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
/** /**
@ -44,6 +44,7 @@ interface RequestCardProps {
export function RequestCard({ request, index, onViewRequest }: RequestCardProps) { export function RequestCard({ request, index, onViewRequest }: RequestCardProps) {
const statusConfig = getStatusConfig(request.status); const statusConfig = getStatusConfig(request.status);
const stateConfig = getWorkflowStateConfig(request.workflowState || (request.status === 'draft' ? 'DRAFT' : 'OPEN'));
const priorityConfig = getPriorityConfig(request.priority); const priorityConfig = getPriorityConfig(request.priority);
const StatusIcon = statusConfig.icon; const StatusIcon = statusConfig.icon;
const PriorityIcon = priorityConfig.icon; const PriorityIcon = priorityConfig.icon;
@ -79,6 +80,15 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<StatusIcon className="w-3 h-3 mr-1" /> <StatusIcon className="w-3 h-3 mr-1" />
<span className="capitalize">{request.status}</span> <span className="capitalize">{request.status}</span>
</Badge> </Badge>
{stateConfig.label.toLowerCase() !== request.status.toLowerCase() && (
<Badge
variant="outline"
className={`${stateConfig.color} border font-medium text-xs shrink-0`}
data-testid="state-badge"
>
<span className="capitalize">{stateConfig.label}</span>
</Badge>
)}
{(request.pauseInfo?.isPaused || (request as any).isPaused) && ( {(request.pauseInfo?.isPaused || (request as any).isPaused) && (
<Badge <Badge
variant="outline" variant="outline"

View File

@ -14,6 +14,7 @@ interface UseMyRequestsOptions {
status?: string; status?: string;
priority?: string; priority?: string;
templateType?: string; templateType?: string;
lifecycle?: string;
}; };
} }
@ -29,7 +30,7 @@ export function useMyRequests({ itemsPerPage = 10 }: UseMyRequestsOptions = {})
}); });
const fetchMyRequests = useCallback( const fetchMyRequests = useCallback(
async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; templateType?: string }) => { async (page: number = 1, filters?: { search?: string; status?: string; priority?: string; templateType?: string, lifecycle?: string }) => {
try { try {
if (page === 1) { if (page === 1) {
setLoading(true); setLoading(true);
@ -43,6 +44,7 @@ export function useMyRequests({ itemsPerPage = 10 }: UseMyRequestsOptions = {})
status: filters?.status, status: filters?.status,
priority: filters?.priority, priority: filters?.priority,
templateType: filters?.templateType, templateType: filters?.templateType,
lifecycle: filters?.lifecycle,
}); });
// Extract data - workflowApi now returns { data: [], pagination: {} } // Extract data - workflowApi now returns { data: [], pagination: {} }

View File

@ -11,6 +11,7 @@ import {
setPriorityFilter as setPriorityFilterAction, setPriorityFilter as setPriorityFilterAction,
setTemplateTypeFilter as setTemplateTypeFilterAction, setTemplateTypeFilter as setTemplateTypeFilterAction,
setCurrentPage as setCurrentPageAction, setCurrentPage as setCurrentPageAction,
setLifecycleFilter as setLifecycleFilterAction,
clearFilters as clearFiltersAction, clearFilters as clearFiltersAction,
} from '../redux/myRequestsSlice'; } from '../redux/myRequestsSlice';
@ -25,7 +26,7 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
// Get filters from Redux // 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 // Create setters that dispatch Redux actions
const setSearchTerm = useCallback((value: string) => dispatch(setSearchTermAction(value)), [dispatch]); const setSearchTerm = useCallback((value: string) => dispatch(setSearchTermAction(value)), [dispatch]);
@ -33,6 +34,7 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
const setPriorityFilter = useCallback((value: string) => dispatch(setPriorityFilterAction(value)), [dispatch]); const setPriorityFilter = useCallback((value: string) => dispatch(setPriorityFilterAction(value)), [dispatch]);
const setTemplateTypeFilter = useCallback((value: string) => dispatch(setTemplateTypeFilterAction(value)), [dispatch]); const setTemplateTypeFilter = useCallback((value: string) => dispatch(setTemplateTypeFilterAction(value)), [dispatch]);
const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]); const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
const setLifecycleFilter = useCallback((value: string) => dispatch(setLifecycleFilterAction(value)), [dispatch]);
const getFilters = useCallback((): MyRequestsFilters => { const getFilters = useCallback((): MyRequestsFilters => {
return { return {
@ -40,8 +42,9 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
status: statusFilter, status: statusFilter,
priority: priorityFilter, priority: priorityFilter,
templateType: templateTypeFilter, templateType: templateTypeFilter,
lifecycle: lifecycleFilter,
}; };
}, [searchTerm, statusFilter, priorityFilter, templateTypeFilter]); }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, lifecycleFilter]);
// Debounced filter change handler // Debounced filter change handler
useEffect(() => { useEffect(() => {
@ -68,7 +71,7 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
clearTimeout(debounceTimeoutRef.current); clearTimeout(debounceTimeoutRef.current);
} }
}; };
}, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, onFiltersChange, getFilters, debounceMs]); }, [searchTerm, statusFilter, priorityFilter, templateTypeFilter, lifecycleFilter, onFiltersChange, getFilters, debounceMs]);
const resetFilters = useCallback(() => { const resetFilters = useCallback(() => {
dispatch(clearFiltersAction()); dispatch(clearFiltersAction());
@ -80,11 +83,13 @@ export function useMyRequestsFilters({ onFiltersChange, debounceMs = 500 }: UseM
priorityFilter, priorityFilter,
templateTypeFilter, templateTypeFilter,
currentPage, currentPage,
lifecycleFilter,
setSearchTerm, setSearchTerm,
setStatusFilter, setStatusFilter,
setPriorityFilter, setPriorityFilter,
setTemplateTypeFilter, setTemplateTypeFilter,
setCurrentPage, setCurrentPage,
setLifecycleFilter,
getFilters, getFilters,
resetFilters, resetFilters,
}; };

View File

@ -6,6 +6,7 @@ export interface MyRequestsFiltersState {
priorityFilter: string; priorityFilter: string;
templateTypeFilter: string; templateTypeFilter: string;
currentPage: number; currentPage: number;
lifecycleFilter: string;
} }
const initialState: MyRequestsFiltersState = { const initialState: MyRequestsFiltersState = {
@ -14,6 +15,7 @@ const initialState: MyRequestsFiltersState = {
priorityFilter: 'all', priorityFilter: 'all',
templateTypeFilter: 'all', templateTypeFilter: 'all',
currentPage: 1, currentPage: 1,
lifecycleFilter: 'all',
}; };
const myRequestsSlice = createSlice({ const myRequestsSlice = createSlice({
@ -37,12 +39,16 @@ const myRequestsSlice = createSlice({
setCurrentPage: (state, action: PayloadAction<number>) => { setCurrentPage: (state, action: PayloadAction<number>) => {
state.currentPage = action.payload; state.currentPage = action.payload;
}, },
setLifecycleFilter: (state, action: PayloadAction<string>) => {
state.lifecycleFilter = action.payload;
},
clearFilters: (state) => { clearFilters: (state) => {
state.searchTerm = ''; state.searchTerm = '';
state.statusFilter = 'all'; state.statusFilter = 'all';
state.priorityFilter = 'all'; state.priorityFilter = 'all';
state.templateTypeFilter = 'all'; state.templateTypeFilter = 'all';
state.currentPage = 1; state.currentPage = 1;
state.lifecycleFilter = 'all';
}, },
}, },
}); });
@ -53,6 +59,7 @@ export const {
setPriorityFilter, setPriorityFilter,
setTemplateTypeFilter, setTemplateTypeFilter,
setCurrentPage, setCurrentPage,
setLifecycleFilter,
clearFilters, clearFilters,
} = myRequestsSlice.actions; } = myRequestsSlice.actions;

View File

@ -17,6 +17,7 @@ export interface MyRequest {
approverLevel?: string; approverLevel?: string;
templateType?: string; templateType?: string;
workflowType?: string; workflowType?: string;
workflowState?: string;
templateName?: string; templateName?: string;
pauseInfo?: { pauseInfo?: {
isPaused: boolean; isPaused: boolean;
@ -41,6 +42,7 @@ export interface MyRequestsFilters {
status: string; status: string;
priority: string; priority: string;
templateType?: string; templateType?: string;
lifecycle?: string;
} }
export interface PaginationState { export interface PaginationState {

View File

@ -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',
};
}
}

View File

@ -31,6 +31,7 @@ export function transformRequest(req: any): MyRequest {
: '—', : '—',
templateType: req.templateType || req.template_type, templateType: req.templateType || req.template_type,
workflowType: req.workflowType || req.workflow_type, workflowType: req.workflowType || req.workflow_type,
workflowState: req.workflowState || req.workflow_state,
templateName: req.templateName || req.template_name, templateName: req.templateName || req.template_name,
}; };
} }

View File

@ -57,7 +57,7 @@ export function QuickActionsSidebar({
const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]); const [sharedRecipients, setSharedRecipients] = useState<SharedRecipient[]>([]);
const [loadingRecipients, setLoadingRecipients] = useState(false); const [loadingRecipients, setLoadingRecipients] = useState(false);
const [hasRetriggerNotification, setHasRetriggerNotification] = 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 isPaused = request?.pauseInfo?.isPaused || false;
const pausedByUserId = pausedByUserIdProp || request?.pauseInfo?.pausedBy?.userId; const pausedByUserId = pausedByUserIdProp || request?.pauseInfo?.pausedBy?.userId;
const currentUserId = currentUserIdProp || (user as any)?.userId || ''; const currentUserId = currentUserIdProp || (user as any)?.userId || '';

View File

@ -5,7 +5,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { ArrowLeft, FileText, RefreshCw, Share2 } from 'lucide-react'; 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'; import { SLAProgressBar } from '@/components/sla/SLAProgressBar';
interface RequestDetailHeaderProps { interface RequestDetailHeaderProps {
@ -32,6 +32,7 @@ export function RequestDetailHeader({
}: RequestDetailHeaderProps) { }: RequestDetailHeaderProps) {
const priorityConfig = getPriorityConfig(request?.priority || 'standard'); const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending'); const statusConfig = getStatusConfig(request?.status || 'pending');
const stateConfig = getWorkflowStateConfig(request?.workflowState || (request?.status === 'DRAFT' ? 'DRAFT' : 'OPEN'));
return ( return (
<div className="bg-white rounded-lg shadow-sm border border-gray-300 mb-4 sm:mb-6" data-testid="request-detail-header"> <div className="bg-white rounded-lg shadow-sm border border-gray-300 mb-4 sm:mb-6" data-testid="request-detail-header">
@ -77,6 +78,15 @@ export function RequestDetailHeader({
> >
{statusConfig.label} {statusConfig.label}
</Badge> </Badge>
{stateConfig.label.toLowerCase() !== (request?.status || '').toLowerCase() && (
<Badge
className={`${stateConfig.color} rounded-full px-2 sm:px-3 text-xs capitalize shrink-0`}
variant="outline"
data-testid="state-badge"
>
{stateConfig.label}
</Badge>
)}
{/* Template Type Badge */} {/* Template Type Badge */}
{(() => { {(() => {
const workflowType = request?.workflowType || request?.workflow_type; const workflowType = request?.workflowType || request?.workflow_type;
@ -120,7 +130,7 @@ export function RequestDetailHeader({
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Share Summary Button - Only show for closed requests if user is initiator */} {/* Share Summary Button - Only show for closed requests if user is initiator */}
{onShareSummary && isInitiator && request?.status?.toLowerCase() === 'closed' && ( {onShareSummary && isInitiator && request?.workflowState?.toLowerCase() === 'closed' && (
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
@ -157,8 +167,7 @@ export function RequestDetailHeader({
{/* SLA Progress Section - Plug-and-play: Module passes prepared SLA data */} {/* SLA Progress Section - Plug-and-play: Module passes prepared SLA data */}
{slaData !== undefined && ( {slaData !== undefined && (
<div className={`px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-gray-200 ${ <div className={`px-3 sm:px-4 md:px-6 py-3 sm:py-4 border-b border-gray-200 ${isPaused ? 'bg-gradient-to-r from-gray-100 to-gray-200' : 'bg-gradient-to-r from-blue-50 to-indigo-50'
isPaused ? 'bg-gradient-to-r from-gray-100 to-gray-200' : 'bg-gradient-to-r from-blue-50 to-indigo-50'
}`} data-testid="sla-section"> }`} data-testid="sla-section">
<SLAProgressBar <SLAProgressBar
sla={slaData} sla={slaData}

View File

@ -35,6 +35,7 @@ interface OverviewTabProps {
generationAttempts?: number; generationAttempts?: number;
generationFailed?: boolean; generationFailed?: boolean;
maxAttemptsReached?: boolean; maxAttemptsReached?: boolean;
isClosed?: boolean;
} }
export function OverviewTab({ export function OverviewTab({
@ -57,6 +58,7 @@ export function OverviewTab({
generationAttempts = 0, generationAttempts = 0,
generationFailed = false, generationFailed = false,
maxAttemptsReached = false, maxAttemptsReached = false,
isClosed = false,
}: OverviewTabProps) { }: OverviewTabProps) {
void _onPause; // Marked as intentionally unused - available for future use void _onPause; // Marked as intentionally unused - available for future use
const { user } = useAuth(); const { user } = useAuth();
@ -301,7 +303,7 @@ export function OverviewTab({
)} )}
{/* Read-Only Conclusion Remark */} {/* Read-Only Conclusion Remark */}
{request.status === 'closed' && request.conclusionRemark && ( {isClosed && request.conclusionRemark && (
<Card> <Card>
<CardHeader className="bg-gradient-to-r from-gray-50 to-slate-50 border-b border-gray-200"> <CardHeader className="bg-gradient-to-r from-gray-50 to-slate-50 border-b border-gray-200">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
@ -331,18 +333,15 @@ export function OverviewTab({
{/* Conclusion Remark Section */} {/* Conclusion Remark Section */}
{needsClosure && ( {needsClosure && (
<Card data-testid="conclusion-remark-card"> <Card data-testid="conclusion-remark-card">
<CardHeader className={`bg-gradient-to-r border-b ${ <CardHeader className={`bg-gradient-to-r border-b ${request.status === 'rejected'
request.status === 'rejected'
? 'from-red-50 to-rose-50 border-red-200' ? 'from-red-50 to-rose-50 border-red-200'
: 'from-green-50 to-emerald-50 border-green-200' : 'from-green-50 to-emerald-50 border-green-200'
}`}> }`}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div> <div>
<CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${ <CardTitle className={`flex items-center gap-2 text-base sm:text-lg ${request.status === 'rejected' ? 'text-red-700' : 'text-green-700'
request.status === 'rejected' ? 'text-red-700' : 'text-green-700'
}`}> }`}>
<CheckCircle className={`w-5 h-5 ${ <CheckCircle className={`w-5 h-5 ${request.status === 'rejected' ? 'text-red-600' : 'text-green-600'
request.status === 'rejected' ? 'text-red-600' : 'text-green-600'
}`} /> }`} />
Conclusion Remark - Final Step Conclusion Remark - Final Step
</CardTitle> </CardTitle>

View File

@ -163,7 +163,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
}).length; }).length;
const closed = filteredData.filter((r: any) => { const closed = filteredData.filter((r: any) => {
const status = (r.status || '').toString().toUpperCase(); 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; }).length;
setBackendStats({ setBackendStats({
@ -396,6 +397,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange, dateRange: filters.dateRange,
customStartDate: filters.customStartDate, customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate, customEndDate: filters.customEndDate,
lifecycleFilter: filters.lifecycleFilter,
isOrgLevel, isOrgLevel,
}); });
const hasInitialFetchRun = useRef(false); const hasInitialFetchRun = useRef(false);
@ -426,6 +428,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
prev.dateRange !== filters.dateRange || prev.dateRange !== filters.dateRange ||
prev.customStartDate !== filters.customStartDate || prev.customStartDate !== filters.customStartDate ||
prev.customEndDate !== filters.customEndDate || prev.customEndDate !== filters.customEndDate ||
prev.lifecycleFilter !== filters.lifecycleFilter ||
prev.isOrgLevel !== isOrgLevel; prev.isOrgLevel !== isOrgLevel;
if (!hasChanged) return; if (!hasChanged) return;
@ -447,6 +450,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange, dateRange: filters.dateRange,
customStartDate: filters.customStartDate, customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate, customEndDate: filters.customEndDate,
lifecycleFilter: filters.lifecycleFilter,
isOrgLevel, isOrgLevel,
}; };
}, filters.searchTerm !== prev.searchTerm ? 500 : 0); }, filters.searchTerm !== prev.searchTerm ? 500 : 0);
@ -466,7 +470,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
filters.approverFilterType, filters.approverFilterType,
filters.dateRange, filters.dateRange,
filters.customStartDate, filters.customStartDate,
filters.customEndDate filters.customEndDate,
filters.lifecycleFilter
]); ]);
// Page change handler // Page change handler
@ -553,8 +558,8 @@ export function Requests({ onViewRequest }: RequestsProps) {
<Separator /> <Separator />
{/* Primary Filters */} {/* Primary Filters */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3 sm:gap-4">
<div className="relative md:col-span-3 lg:col-span-1"> <div className="relative md:col-span-2 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
placeholder="Search requests..." placeholder="Search requests..."
@ -565,6 +570,17 @@ export function Requests({ onViewRequest }: RequestsProps) {
/> />
</div> </div>
<Select value={filters.lifecycleFilter} onValueChange={filters.setLifecycleFilter}>
<SelectTrigger className="h-10" data-testid="lifecycle-filter">
<SelectValue placeholder="Lifecycle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Requests</SelectItem>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
<Select value={filters.statusFilter} onValueChange={filters.setStatusFilter}> <Select value={filters.statusFilter} onValueChange={filters.setStatusFilter}>
<SelectTrigger className="h-10" data-testid="status-filter"> <SelectTrigger className="h-10" data-testid="status-filter">
<SelectValue placeholder="All Status" /> <SelectValue placeholder="All Status" />
@ -575,7 +591,6 @@ export function Requests({ onViewRequest }: RequestsProps) {
<SelectItem value="paused">Paused</SelectItem> <SelectItem value="paused">Paused</SelectItem>
<SelectItem value="approved">Approved</SelectItem> <SelectItem value="approved">Approved</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem> <SelectItem value="rejected">Rejected</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -327,6 +327,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange, dateRange: filters.dateRange,
customStartDate: filters.customStartDate, customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate, customEndDate: filters.customEndDate,
lifecycleFilter: filters.lifecycleFilter,
}); });
const hasInitialFetchRun = useRef(false); const hasInitialFetchRun = useRef(false);
@ -355,7 +356,8 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
prev.approverFilterType !== filters.approverFilterType || prev.approverFilterType !== filters.approverFilterType ||
prev.dateRange !== filters.dateRange || prev.dateRange !== filters.dateRange ||
prev.customStartDate !== filters.customStartDate || prev.customStartDate !== filters.customStartDate ||
prev.customEndDate !== filters.customEndDate; prev.customEndDate !== filters.customEndDate ||
prev.lifecycleFilter !== filters.lifecycleFilter;
if (!hasChanged) return; if (!hasChanged) return;
@ -376,6 +378,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
dateRange: filters.dateRange, dateRange: filters.dateRange,
customStartDate: filters.customStartDate, customStartDate: filters.customStartDate,
customEndDate: filters.customEndDate, customEndDate: filters.customEndDate,
lifecycleFilter: filters.lifecycleFilter,
}; };
}, filters.searchTerm !== prev.searchTerm ? 500 : 0); }, filters.searchTerm !== prev.searchTerm ? 500 : 0);
@ -393,7 +396,9 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
filters.approverFilterType, filters.approverFilterType,
filters.dateRange, filters.dateRange,
filters.customStartDate, filters.customStartDate,
filters.customEndDate filters.customStartDate,
filters.customEndDate,
filters.lifecycleFilter
]); ]);
// Page change handler // Page change handler
@ -477,6 +482,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
{/* Filters - Plug-and-play pattern */} {/* Filters - Plug-and-play pattern */}
<UserAllRequestsFiltersComponent <UserAllRequestsFiltersComponent
searchTerm={filters.searchTerm} searchTerm={filters.searchTerm}
lifecycleFilter={filters.lifecycleFilter}
statusFilter={filters.statusFilter} statusFilter={filters.statusFilter}
priorityFilter={filters.priorityFilter} priorityFilter={filters.priorityFilter}
templateTypeFilter={filters.templateTypeFilter} templateTypeFilter={filters.templateTypeFilter}
@ -494,6 +500,7 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) {
initiatorSearch={initiatorSearch} initiatorSearch={initiatorSearch}
approverSearch={approverSearch} approverSearch={approverSearch}
onSearchChange={filters.setSearchTerm} onSearchChange={filters.setSearchTerm}
onLifecycleChange={filters.setLifecycleFilter}
onStatusChange={filters.setStatusFilter} onStatusChange={filters.setStatusFilter}
onPriorityChange={filters.setPriorityFilter} onPriorityChange={filters.setPriorityFilter}
onTemplateTypeChange={filters.setTemplateTypeFilter} onTemplateTypeChange={filters.setTemplateTypeFilter}

View File

@ -6,7 +6,7 @@ import { motion } from 'framer-motion';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { User, ArrowRight, TrendingUp, Clock, Pause } from 'lucide-react'; import { User, ArrowRight, TrendingUp, Clock, Pause } from 'lucide-react';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig, getWorkflowStateConfig } from '../utils/configMappers';
import type { ConvertedRequest } from '../types/requests.types'; import type { ConvertedRequest } from '../types/requests.types';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
@ -43,6 +43,7 @@ interface RequestCardProps {
export function RequestCard({ request, index, onViewRequest }: RequestCardProps) { export function RequestCard({ request, index, onViewRequest }: RequestCardProps) {
const statusConfig = getStatusConfig(request.status); const statusConfig = getStatusConfig(request.status);
const stateConfig = getWorkflowStateConfig(request.workflowState || (request.status === 'draft' ? 'DRAFT' : 'OPEN'));
const priorityConfig = getPriorityConfig(request.priority); const priorityConfig = getPriorityConfig(request.priority);
const StatusIcon = statusConfig.icon; const StatusIcon = statusConfig.icon;
const PriorityIcon = priorityConfig.icon; const PriorityIcon = priorityConfig.icon;
@ -78,6 +79,15 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<StatusIcon className="w-3 h-3 mr-1" /> <StatusIcon className="w-3 h-3 mr-1" />
<span className="capitalize">{request.status}</span> <span className="capitalize">{request.status}</span>
</Badge> </Badge>
{stateConfig.label.toLowerCase() !== (request.status || '').toLowerCase() && (
<Badge
variant="outline"
className={`${stateConfig.color} border font-medium text-xs shrink-0`}
data-testid="state-badge"
>
<span className="capitalize">{stateConfig.label}</span>
</Badge>
)}
{((request as any).pauseInfo?.isPaused || (request as any).isPaused) && ( {((request as any).pauseInfo?.isPaused || (request as any).isPaused) && (
<Badge <Badge
variant="outline" variant="outline"

View File

@ -3,7 +3,7 @@
* Displays statistics cards for requests with click handlers to filter * Displays statistics cards for requests with click handlers to filter
*/ */
import { FileText, Clock, Pause, CheckCircle, XCircle, Archive } from 'lucide-react'; import { FileText, Clock, Pause, CheckCircle, XCircle } from 'lucide-react';
import { StatsCard } from '@/components/dashboard/StatsCard'; import { StatsCard } from '@/components/dashboard/StatsCard';
import type { RequestStats } from '../types/requests.types'; import type { RequestStats } from '../types/requests.types';
@ -20,7 +20,7 @@ export function RequestsStats({ stats, onStatusFilter }: RequestsStatsProps) {
}; };
return ( return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4" data-testid="requests-stats"> <div className="grid grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-4" data-testid="requests-stats">
<StatsCard <StatsCard
label="Total" label="Total"
value={stats.total} value={stats.total}
@ -80,18 +80,6 @@ export function RequestsStats({ stats, onStatusFilter }: RequestsStatsProps) {
testId="stat-rejected" testId="stat-rejected"
onClick={onStatusFilter ? () => handleCardClick('rejected') : undefined} onClick={onStatusFilter ? () => handleCardClick('rejected') : undefined}
/> />
<StatsCard
label="Closed"
value={stats.closed}
icon={Archive}
iconColor="text-purple-600"
gradient="bg-gradient-to-br from-purple-50 to-purple-100 border-purple-200"
textColor="text-purple-700"
valueColor="text-purple-900"
testId="stat-closed"
onClick={onStatusFilter ? () => handleCardClick('closed') : undefined}
/>
</div> </div>
); );
} }

View File

@ -22,6 +22,7 @@ import {
setCustomEndDate as setCustomEndDateAction, setCustomEndDate as setCustomEndDateAction,
setShowCustomDatePicker as setShowCustomDatePickerAction, setShowCustomDatePicker as setShowCustomDatePickerAction,
setCurrentPage as setCurrentPageAction, setCurrentPage as setCurrentPageAction,
setLifecycleFilter as setLifecycleFilterAction,
clearFilters as clearFiltersAction, clearFilters as clearFiltersAction,
} from '../redux/requestsSlice'; } from '../redux/requestsSlice';
@ -44,6 +45,7 @@ export function useRequestsFilters() {
customEndDate, customEndDate,
showCustomDatePicker, showCustomDatePicker,
currentPage, currentPage,
lifecycleFilter,
} = useAppSelector((state) => state.requests); } = useAppSelector((state) => state.requests);
// Create setters that dispatch Redux actions // Create setters that dispatch Redux actions
@ -61,6 +63,7 @@ export function useRequestsFilters() {
const setCustomEndDate = useCallback((value: Date | undefined) => dispatch(setCustomEndDateAction(value)), [dispatch]); const setCustomEndDate = useCallback((value: Date | undefined) => dispatch(setCustomEndDateAction(value)), [dispatch]);
const setShowCustomDatePicker = useCallback((value: boolean) => dispatch(setShowCustomDatePickerAction(value)), [dispatch]); const setShowCustomDatePicker = useCallback((value: boolean) => dispatch(setShowCustomDatePickerAction(value)), [dispatch]);
const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]); const setCurrentPage = useCallback((value: number) => dispatch(setCurrentPageAction(value)), [dispatch]);
const setLifecycleFilter = useCallback((value: string) => dispatch(setLifecycleFilterAction(value)), [dispatch]);
const getFilters = useCallback((): RequestFilters => { const getFilters = useCallback((): RequestFilters => {
return { return {
@ -73,6 +76,7 @@ export function useRequestsFilters() {
initiator: initiatorFilter !== 'all' ? initiatorFilter : undefined, initiator: initiatorFilter !== 'all' ? initiatorFilter : undefined,
approver: approverFilter !== 'all' ? approverFilter : undefined, approver: approverFilter !== 'all' ? approverFilter : undefined,
approverType: approverFilter !== 'all' ? approverFilterType : undefined, approverType: approverFilter !== 'all' ? approverFilterType : undefined,
lifecycle: lifecycleFilter !== 'all' ? lifecycleFilter : undefined,
dateRange, dateRange,
startDate: customStartDate, startDate: customStartDate,
endDate: customEndDate endDate: customEndDate
@ -87,6 +91,7 @@ export function useRequestsFilters() {
initiatorFilter, initiatorFilter,
approverFilter, approverFilter,
approverFilterType, approverFilterType,
lifecycleFilter, // Ensure lifecycleFilter is in dependencies
dateRange, dateRange,
customStartDate, customStartDate,
customEndDate customEndDate
@ -128,6 +133,7 @@ export function useRequestsFilters() {
departmentFilter !== 'all' || departmentFilter !== 'all' ||
initiatorFilter !== 'all' || initiatorFilter !== 'all' ||
approverFilter !== 'all' || approverFilter !== 'all' ||
lifecycleFilter !== 'all' ||
dateRange !== 'all' || dateRange !== 'all' ||
customStartDate || customStartDate ||
customEndDate customEndDate
@ -147,6 +153,7 @@ export function useRequestsFilters() {
dateRange, dateRange,
customStartDate, customStartDate,
customEndDate, customEndDate,
lifecycleFilter,
showCustomDatePicker, showCustomDatePicker,
currentPage, currentPage,
hasActiveFilters, hasActiveFilters,
@ -165,6 +172,7 @@ export function useRequestsFilters() {
setCustomEndDate, setCustomEndDate,
setShowCustomDatePicker, setShowCustomDatePicker,
setCurrentPage, setCurrentPage,
setLifecycleFilter,
// Helpers // Helpers
getFilters, getFilters,
clearFilters, clearFilters,

View File

@ -16,6 +16,7 @@ export interface RequestsFiltersState {
customEndDate?: Date; customEndDate?: Date;
showCustomDatePicker: boolean; showCustomDatePicker: boolean;
currentPage: number; currentPage: number;
lifecycleFilter: string;
} }
const initialState: RequestsFiltersState = { const initialState: RequestsFiltersState = {
@ -33,6 +34,7 @@ const initialState: RequestsFiltersState = {
customEndDate: undefined, customEndDate: undefined,
showCustomDatePicker: false, showCustomDatePicker: false,
currentPage: 1, currentPage: 1,
lifecycleFilter: 'all',
}; };
const requestsSlice = createSlice({ const requestsSlice = createSlice({
@ -81,6 +83,9 @@ const requestsSlice = createSlice({
setCurrentPage: (state, action: PayloadAction<number>) => { setCurrentPage: (state, action: PayloadAction<number>) => {
state.currentPage = action.payload; state.currentPage = action.payload;
}, },
setLifecycleFilter: (state, action: PayloadAction<string>) => {
state.lifecycleFilter = action.payload;
},
clearFilters: (state) => { clearFilters: (state) => {
state.searchTerm = ''; state.searchTerm = '';
state.statusFilter = 'all'; state.statusFilter = 'all';
@ -96,6 +101,7 @@ const requestsSlice = createSlice({
state.customEndDate = undefined; state.customEndDate = undefined;
state.showCustomDatePicker = false; state.showCustomDatePicker = false;
state.currentPage = 1; state.currentPage = 1;
state.lifecycleFilter = 'all';
}, },
}, },
}); });
@ -115,6 +121,7 @@ export const {
setCustomEndDate, setCustomEndDate,
setShowCustomDatePicker, setShowCustomDatePicker,
setCurrentPage, setCurrentPage,
setLifecycleFilter,
clearFilters, clearFilters,
} = requestsSlice.actions; } = requestsSlice.actions;

View File

@ -36,6 +36,7 @@ export async function fetchRequestsData({
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange; if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString(); if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString();
if (filters?.endDate) backendFilters.endDate = filters.endDate?.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) // Fetch paginated data for list display (with status filter)
const pageResult = await workflowApi.listWorkflows({ const pageResult = await workflowApi.listWorkflows({
@ -98,6 +99,7 @@ export async function fetchRequestsData({
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange; if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString(); if (filters?.startDate) backendFilters.startDate = filters.startDate?.toISOString();
if (filters?.endDate) backendFilters.endDate = filters.endDate?.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 // Fetch paginated data using endpoint for regular users
// This endpoint includes all requests where user is initiator, approver, or participant // This endpoint includes all requests where user is initiator, approver, or participant

View File

@ -41,6 +41,7 @@ export async function fetchUserParticipantRequestsData({
if (filters?.dateRange) backendFilters.dateRange = filters.dateRange; if (filters?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate; 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?.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 // Use single optimized endpoint - listParticipantRequests now includes initiator requests
// Only fetch the requested page (10 records) for optimal performance // 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?.dateRange) backendFilters.dateRange = filters.dateRange;
if (filters?.startDate) backendFilters.startDate = filters.startDate instanceof Date ? filters.startDate.toISOString() : filters.startDate; 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?.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 // Fetch all pages using the single optimized endpoint
while (hasMore && currentPage <= maxPages) { while (hasMore && currentPage <= maxPages) {
@ -150,4 +152,3 @@ export async function fetchAllRequestsForExport(filters?: RequestFilters): Promi
return allPages; return allPages;
} }

View File

@ -21,6 +21,7 @@ export interface RequestFilters {
dateRange?: DateRange; dateRange?: DateRange;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
lifecycle?: string;
} }
export interface RequestStats { export interface RequestStats {
@ -64,6 +65,7 @@ export interface ConvertedRequest {
approverLevel: string; approverLevel: string;
templateType?: string; templateType?: string;
workflowType?: string; workflowType?: string;
workflowState?: string;
templateName?: string; templateName?: string;
} }

View File

@ -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'
};
}
};

View File

@ -90,7 +90,7 @@ export function transformRequest(req: any): ConvertedRequest {
displayId: req.requestNumber || req.request_number || req.id, displayId: req.requestNumber || req.request_number || req.id,
title: req.title, title: req.title,
description: req.description, description: req.description,
status: status.toLowerCase().replace('_','-'), status: status.toLowerCase().replace('_', '-'),
priority: priority, priority: priority,
department: req.department || req.initiator?.department, department: req.department || req.initiator?.department,
submittedDate: req.submittedAt || (req.createdAt ? new Date(req.createdAt).toISOString().split('T')[0] : undefined), 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, approverLevel: approverLevel,
templateType: req.templateType || req.template_type, templateType: req.templateType || req.template_type,
workflowType: req.workflowType || req.workflow_type, workflowType: req.workflowType || req.workflow_type,
workflowState: req.workflowState || req.workflow_state,
templateName: req.templateName || req.template_name templateName: req.templateName || req.template_name
}; };
} }

View File

@ -102,6 +102,8 @@ export interface CriticalRequest {
isCritical: boolean; isCritical: boolean;
approverId?: string | null; approverId?: string | null;
approverEmail?: string | null; approverEmail?: string | null;
isActionable?: boolean;
requestRole?: 'APPROVER' | 'INITIATOR' | 'PARTICIPANT';
} }
export interface AIRemarkUtilization { export interface AIRemarkUtilization {
@ -203,7 +205,8 @@ class DashboardService {
approverType?: 'current' | 'any', approverType?: 'current' | 'any',
search?: string, search?: string,
slaCompliance?: string, slaCompliance?: string,
viewAsUser?: boolean viewAsUser?: boolean,
lifecycle?: string
): Promise<RequestStats> { ): Promise<RequestStats> {
try { try {
const params: any = { dateRange }; const params: any = { dateRange };
@ -215,6 +218,9 @@ class DashboardService {
if (status && status !== 'all') { if (status && status !== 'all') {
params.status = status; params.status = status;
} }
if (lifecycle && lifecycle !== 'all') {
params.lifecycle = lifecycle;
}
if (priority && priority !== 'all') { if (priority && priority !== 'all') {
params.priority = priority; params.priority = priority;
} }

View File

@ -155,8 +155,8 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
return { id: data?.requestId } as any; 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 } = {}) { 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, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, templateType, department, initiator, approver, slaCompliance, lifecycle, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows', { const res = await apiClient.get('/workflows', {
params: { params: {
page, page,
@ -169,6 +169,7 @@ export async function listWorkflows(params: { page?: number; limit?: number; sea
initiator, initiator,
approver, approver,
slaCompliance, slaCompliance,
lifecycle,
dateRange, dateRange,
startDate, startDate,
endDate endDate
@ -179,8 +180,8 @@ export async function listWorkflows(params: { page?: number; limit?: number; sea
// List requests where user is a participant (not initiator) - for regular users' "All Requests" page // List requests where user is a participant (not initiator) - for regular users' "All Requests" page
// SEPARATE from listWorkflows (admin) to avoid interference // 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 } = {}) { 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, dateRange, startDate, endDate } = params; 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', { const res = await apiClient.get('/workflows/participant-requests', {
params: { params: {
page, page,
@ -194,6 +195,7 @@ export async function listParticipantRequests(params: { page?: number; limit?: n
approver, approver,
approverType, approverType,
slaCompliance, slaCompliance,
lifecycle,
dateRange, dateRange,
startDate, startDate,
endDate endDate
@ -234,8 +236,8 @@ export async function listMyWorkflows(params: { page?: number; limit?: number; s
} }
// List requests where user is the initiator - for "My Requests" page // 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 } = {}) { 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, dateRange, startDate, endDate } = params; const { page = 1, limit = 20, search, status, priority, templateType, department, slaCompliance, lifecycle, dateRange, startDate, endDate } = params;
const res = await apiClient.get('/workflows/my-initiated', { const res = await apiClient.get('/workflows/my-initiated', {
params: { params: {
page, page,
@ -246,6 +248,7 @@ export async function listMyInitiatedWorkflows(params: { page?: number; limit?:
templateType, templateType,
department, department,
slaCompliance, slaCompliance,
lifecycle,
dateRange, dateRange,
startDate, startDate,
endDate endDate

View File

@ -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 * Utility: getActionTypeIcon
* *

View File

@ -30,7 +30,7 @@ async function ensureConfigLoaded() {
} }
// Initialize config on first import (non-blocking) // Initialize config on first import (non-blocking)
ensureConfigLoaded().catch(() => {}); ensureConfigLoaded().catch(() => { });
/** /**
* Check if current time is within working hours * Check if current time is within working hours
@ -241,33 +241,21 @@ export function formatHoursMinutes(hours: number | null | undefined): string {
} }
// Calculate days and remaining hours (8 hours = 1 day) // 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 days = Math.floor(hours / WORKING_HOURS_PER_DAY);
const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY); const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY);
const minutes = Math.round((hours % 1) * 60); const minutes = Math.round((hours % 1) * 60);
// If we have days, format with days (matching backend format)
if (days > 0) {
const dayLabel = days === 1 ? 'day' : 'days'; const dayLabel = days === 1 ? 'day' : 'days';
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
const minuteLabel = minutes === 1 ? 'min' : 'm';
if (minutes > 0) { if (days > 0) {
return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; return minutes > 0
} else { ? `${days} ${dayLabel} ${remainingHrs}h ${minutes}min`
return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`; : `${days} ${dayLabel} ${remainingHrs}h`;
}
} }
// No days, just hours and minutes return minutes > 0
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; ? `${remainingHrs}h ${minutes}min`
const minuteLabel = minutes === 1 ? 'min' : 'm'; : `${remainingHrs}h`;
if (minutes > 0) {
return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
} else {
return `${remainingHrs} ${hourLabel}`;
}
} }
/** /**
@ -284,17 +272,17 @@ export function formatWorkingHours(hours: number): string {
const minutes = remainingMinutes % 60; const minutes = remainingMinutes % 60;
if (days > 0 && remainingHours > 0 && minutes > 0) { 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) { } else if (days > 0 && remainingHours > 0) {
return `${days}d ${remainingHours}h`; return `${days}d ${remainingHours}h`;
} else if (days > 0) { } else if (days > 0) {
return `${days}d`; return `${days}d`;
} else if (remainingHours > 0 && minutes > 0) { } else if (remainingHours > 0 && minutes > 0) {
return `${remainingHours}h ${minutes}m`; return `${remainingHours}h ${minutes}min`;
} else if (remainingHours > 0) { } else if (remainingHours > 0) {
return `${remainingHours}h`; return `${remainingHours}h`;
} else { } else {
return `${minutes}m`; return `${minutes}min`;
} }
} }