request detil page enhanced

This commit is contained in:
laxmanhalaki 2025-12-17 13:05:27 +05:30
parent ea6cd5151b
commit ecf2556c64
8 changed files with 203 additions and 27 deletions

View File

@ -27,8 +27,12 @@ export function SLAProgressBar({
isPaused = false, isPaused = false,
testId = 'sla-progress' testId = 'sla-progress'
}: SLAProgressBarProps) { }: SLAProgressBarProps) {
// Pure presentational component - no business logic
// If request is closed/approved/rejected or no SLA data, show status message // If request is closed/approved/rejected or no SLA data, show status message
if (!sla || requestStatus === 'approved' || requestStatus === 'rejected' || requestStatus === 'closed') { // Check if SLA has required fields (percentageUsed or at least some data)
const hasValidSLA = sla && (sla.percentageUsed !== undefined || sla.percent !== undefined || sla.elapsedHours !== undefined);
if (!hasValidSLA || requestStatus === 'approved' || requestStatus === 'rejected' || requestStatus === 'closed') {
return ( return (
<div className="flex items-center gap-2" data-testid={`${testId}-status-only`}> <div className="flex items-center gap-2" data-testid={`${testId}-status-only`}>
{requestStatus === 'closed' ? <Lock className="h-4 w-4 text-gray-600" /> : {requestStatus === 'closed' ? <Lock className="h-4 w-4 text-gray-600" /> :
@ -47,7 +51,8 @@ export function SLAProgressBar({
// Use percentage-based colors to match approver SLA tracker // Use percentage-based colors to match approver SLA tracker
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached) // Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
// Grey: When paused (frozen state) // Grey: When paused (frozen state)
const percentageUsed = sla.percentageUsed || 0; // Handle both full format (percentageUsed) and simplified format (percent)
const percentageUsed = sla.percentageUsed !== undefined ? sla.percentageUsed : (sla.percent || 0);
const rawStatus = sla.status || 'on_track'; const rawStatus = sla.status || 'on_track';
// Determine colors based on percentage (matching ApprovalStepCard logic) // Determine colors based on percentage (matching ApprovalStepCard logic)
@ -117,12 +122,12 @@ export function SLAProgressBar({
className={`text-xs ${colors.badge}`} className={`text-xs ${colors.badge}`}
data-testid={`${testId}-badge`} data-testid={`${testId}-badge`}
> >
{sla.percentageUsed || 0}% elapsed {isPaused && '(frozen)'} {percentageUsed}% elapsed {isPaused && '(frozen)'}
</Badge> </Badge>
</div> </div>
<Progress <Progress
value={sla.percentageUsed || 0} value={percentageUsed}
className="h-3 mb-2" className="h-3 mb-2"
indicatorClassName={colors.progress} indicatorClassName={colors.progress}
data-testid={`${testId}-bar`} data-testid={`${testId}-bar`}
@ -130,7 +135,7 @@ export function SLAProgressBar({
<div className="flex items-center justify-between text-xs mb-1"> <div className="flex items-center justify-between text-xs mb-1">
<span className="text-gray-600" data-testid={`${testId}-elapsed`}> <span className="text-gray-600" data-testid={`${testId}-elapsed`}>
{sla.elapsedText || formatHoursMinutes(sla.elapsedHours || 0)} elapsed {formatHoursMinutes(sla.elapsedHours || 0)} elapsed
</span> </span>
<span <span
className={`font-semibold ${ className={`font-semibold ${
@ -146,7 +151,7 @@ export function SLAProgressBar({
{sla.deadline && ( {sla.deadline && (
<p className="text-xs text-gray-500" data-testid={`${testId}-deadline`}> <p className="text-xs text-gray-500" data-testid={`${testId}-deadline`}>
Due: {formatDateDDMMYYYY(sla.deadline, true)} {sla.percentageUsed || 0}% elapsed Due: {formatDateDDMMYYYY(sla.deadline, true)} {percentageUsed}% elapsed
</p> </p>
)} )}

View File

@ -372,6 +372,9 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
onRefresh={handleRefresh} onRefresh={handleRefresh}
onShareSummary={handleShareSummary} onShareSummary={handleShareSummary}
isInitiator={isInitiator} isInitiator={isInitiator}
// Custom module: Business logic for preparing SLA data
slaData={request?.summary?.sla || request?.sla || null}
isPaused={request?.pauseInfo?.isPaused || false}
/> />
{/* Tabs */} {/* Tabs */}

View File

@ -101,7 +101,12 @@ export function ClaimManagementOverviewTab({
return ( return (
<div className={`space-y-6 ${className}`}> <div className={`space-y-6 ${className}`}>
{/* Activity Information - Always visible */} {/* Activity Information - Always visible */}
<ActivityInformationCard activityInfo={claimRequest.activityInfo} /> {/* Dealer-claim module: Business logic for preparing timestamp data */}
<ActivityInformationCard
activityInfo={claimRequest.activityInfo}
createdAt={apiRequest?.createdAt}
updatedAt={apiRequest?.updatedAt}
/>
{/* Dealer Information - Always visible */} {/* Dealer Information - Always visible */}
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} /> <DealerInformationCard dealerInfo={claimRequest.dealerInfo} />

View File

@ -9,8 +9,10 @@ import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity } from 'lucide-react'; import { Progress } from '@/components/ui/progress';
import { formatDateTime } from '@/utils/dateFormatter'; import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity, AlertTriangle, AlertOctagon } from 'lucide-react';
import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { formatHoursMinutes } from '@/utils/slaTracker';
import { DealerProposalSubmissionModal } from './modals'; import { DealerProposalSubmissionModal } from './modals';
import { InitiatorProposalApprovalModal } from './modals'; import { InitiatorProposalApprovalModal } from './modals';
import { DeptLeadIOApprovalModal } from './modals'; import { DeptLeadIOApprovalModal } from './modals';
@ -337,6 +339,17 @@ export function DealerClaimWorkflowTab({
normalizedStatus = 'in_progress'; normalizedStatus = 'in_progress';
} }
// Business logic: Only show elapsed time for active or completed steps
// Waiting steps (future steps) should have elapsedHours = 0
// This ensures that when in step 1, only step 1 shows elapsed time, others show 0
const isWaiting = normalizedStatus === 'waiting';
const isActive = normalizedStatus === 'pending' || normalizedStatus === 'in_progress';
const isCompleted = normalizedStatus === 'approved' || normalizedStatus === 'rejected';
// Only calculate/show elapsed hours for active or completed steps
// For waiting steps, elapsedHours should be 0 (they haven't started yet)
const elapsedHours = isWaiting ? 0 : (step.elapsedHours || 0);
return { return {
step: step.step || index + 1, step: step.step || index + 1,
title: stepTitles[index] || `Step ${step.step || index + 1}`, title: stepTitles[index] || `Step ${step.step || index + 1}`,
@ -346,7 +359,7 @@ export function DealerClaimWorkflowTab({
status: normalizedStatus as any, status: normalizedStatus as any,
comment: step.comment || approval?.comment, comment: step.comment || approval?.comment,
approvedAt: step.approvedAt || approval?.timestamp, approvedAt: step.approvedAt || approval?.timestamp,
elapsedHours: step.elapsedHours, elapsedHours, // Only non-zero for active/completed steps
ioDetails, ioDetails,
dmsDetails, dmsDetails,
einvoiceUrl: step.step === 7 ? (approval as any)?.einvoiceUrl : undefined, einvoiceUrl: step.step === 7 ? (approval as any)?.einvoiceUrl : undefined,
@ -817,6 +830,19 @@ export function DealerClaimWorkflowTab({
const isActive = (step.status === 'pending' || step.status === 'in_progress') && step.step === currentStep; const isActive = (step.status === 'pending' || step.status === 'in_progress') && step.step === currentStep;
const isCompleted = step.status === 'approved'; const isCompleted = step.status === 'approved';
// Find approval data for this step to get SLA information
// First find the corresponding level in approvalFlow to get levelId
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
const approval = stepLevel?.levelId
? request?.approvals?.find((a: any) => a.levelId === stepLevel.levelId || a.level_id === stepLevel.levelId)
: null;
// Check if step is paused
const isPaused = approval?.status === 'PAUSED' ||
(request?.pauseInfo?.isPaused &&
(request?.pauseInfo?.levelId === approval?.levelId ||
request?.pauseInfo?.level_id === approval?.levelId));
return ( return (
<div <div
key={index} key={index}
@ -871,10 +897,11 @@ export function DealerClaimWorkflowTab({
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p> <p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p> <p className="text-xs text-gray-500">TAT: {formatHoursMinutes(step.tatHours)}</p>
{step.elapsedHours && ( {/* Only show elapsed time for active or completed steps, not for waiting steps */}
{step.elapsedHours && (isActive || isCompleted) && (
<p className="text-xs text-gray-600 font-medium"> <p className="text-xs text-gray-600 font-medium">
Elapsed: {step.elapsedHours}h Elapsed: {formatHoursMinutes(step.elapsedHours)}
</p> </p>
)} )}
</div> </div>
@ -887,6 +914,96 @@ export function DealerClaimWorkflowTab({
</div> </div>
)} )}
{/* Active Approver - SLA Time Tracking (Only show for current active step) */}
{isActive && approval?.sla && (
<div className="mt-3 space-y-3">
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Due by:</span>
<span className="font-medium text-gray-900">
{approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'}
</span>
</div>
{/* Current Approver - Time Tracking */}
<div className={`border rounded-lg p-3 ${
isPaused ? 'bg-gray-100 border-gray-300' :
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
'bg-green-50 border-green-200'
}`}>
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
<Clock className="w-4 h-4" />
Current Approver - Time Tracking {isPaused && '(Paused)'}
</p>
<div className="space-y-2 text-xs mb-3">
<div className="flex justify-between">
<span className="text-gray-600">Time elapsed since assigned:</span>
<span className="font-medium text-gray-900">{approval.sla.elapsedText || '0 hours'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Time used:</span>
<span className="font-medium text-gray-900">
{approval.sla.elapsedText || '0 hours'} / {formatHoursMinutes(step.tatHours)} allocated
</span>
</div>
</div>
{/* Progress Bar */}
<div className="space-y-2">
{(() => {
const percentUsed = approval.sla.percentageUsed || 0;
const getActiveIndicatorColor = () => {
if (isPaused) return 'bg-gray-500';
if (percentUsed >= 100) return 'bg-red-600';
if (percentUsed >= 75) return 'bg-orange-500';
if (percentUsed >= 50) return 'bg-amber-500';
return 'bg-green-600';
};
const getActiveTextColor = () => {
if (isPaused) return 'text-gray-600';
if (percentUsed >= 100) return 'text-red-600';
if (percentUsed >= 75) return 'text-orange-600';
if (percentUsed >= 50) return 'text-amber-600';
return 'text-green-600';
};
return (
<>
<Progress
value={percentUsed}
className="h-3"
indicatorClassName={getActiveIndicatorColor()}
/>
<div className="flex items-center justify-between">
<span className={`text-xs font-semibold ${getActiveTextColor()}`}>
Progress: {Math.min(100, percentUsed)}% of TAT used
</span>
<span className="text-xs font-medium text-gray-700">
{approval.sla.remainingText || '0 hours'} remaining
</span>
</div>
</>
);
})()}
{approval.sla.status === 'breached' && (
<p className="text-xs font-semibold text-center text-red-600 flex items-center justify-center gap-1.5">
<AlertOctagon className="w-4 h-4" />
Deadline Breached
</p>
)}
{approval.sla.status === 'critical' && (
<p className="text-xs font-semibold text-center text-orange-600 flex items-center justify-center gap-1.5">
<AlertTriangle className="w-4 h-4" />
Approaching Deadline
</p>
)}
</div>
</div>
</div>
)}
{/* IO Organization Details (Step 3) - Show when step is approved and has IO details */} {/* IO Organization Details (Step 3) - Show when step is approved and has IO details */}
{step.step === 3 && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && ( {step.step === 3 && step.status === 'approved' && step.ioDetails && step.ioDetails.ioNumber && (
<div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200"> <div className="mt-3 p-3 bg-blue-50 rounded-lg border border-blue-200">

View File

@ -7,13 +7,22 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react'; import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types'; import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { formatDateTime } from '@/utils/dateFormatter';
interface ActivityInformationCardProps { interface ActivityInformationCardProps {
activityInfo: ClaimActivityInfo; activityInfo: ClaimActivityInfo;
className?: string; className?: string;
// Plug-and-play: Pass timestamps from module's business logic
createdAt?: string | Date;
updatedAt?: string | Date;
} }
export function ActivityInformationCard({ activityInfo, className }: ActivityInformationCardProps) { export function ActivityInformationCard({
activityInfo,
className,
createdAt,
updatedAt
}: ActivityInformationCardProps) {
// Defensive check: Ensure activityInfo exists // Defensive check: Ensure activityInfo exists
if (!activityInfo) { if (!activityInfo) {
console.warn('[ActivityInformationCard] activityInfo is missing'); console.warn('[ActivityInformationCard] activityInfo is missing');
@ -169,6 +178,24 @@ export function ActivityInformationCard({ activityInfo, className }: ActivityInf
</p> </p>
</div> </div>
)} )}
{/* Timestamps - Similar to Request Details Card */}
{(createdAt || updatedAt) && (
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-300">
{createdAt && (
<div>
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Created</label>
<p className="text-sm text-gray-900 font-medium mt-1">{formatDateTime(createdAt)}</p>
</div>
)}
{updatedAt && (
<div>
<label className="text-xs font-medium text-gray-500 uppercase tracking-wide">Last Updated</label>
<p className="text-sm text-gray-900 font-medium mt-1">{formatDateTime(updatedAt)}</p>
</div>
)}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -410,6 +410,9 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
onRefresh={handleRefresh} onRefresh={handleRefresh}
onShareSummary={handleShareSummary} onShareSummary={handleShareSummary}
isInitiator={isInitiator} isInitiator={isInitiator}
// Dealer-claim module: Business logic for preparing SLA data
slaData={request?.summary?.sla || request?.sla || null}
isPaused={request?.pauseInfo?.isPaused || false}
/> />
{/* Tabs */} {/* Tabs */}

View File

@ -331,6 +331,9 @@ export function useRequestDetails(
documents: mappedDocuments, documents: mappedDocuments,
spectators, spectators,
summary, // Backend-provided SLA summary summary, // Backend-provided SLA summary
// Ensure SLA is available at root level for RequestDetailHeader
// Backend provides full SLA in summary.sla with all required fields
sla: summary?.sla || wf.sla || null,
initiator: { initiator: {
name: wf.initiator?.displayName || wf.initiator?.email, name: wf.initiator?.displayName || wf.initiator?.email,
role: wf.initiator?.designation || undefined, role: wf.initiator?.designation || undefined,

View File

@ -15,12 +15,23 @@ interface RequestDetailHeaderProps {
onRefresh: () => void; onRefresh: () => void;
onShareSummary?: () => void; onShareSummary?: () => void;
isInitiator?: boolean; isInitiator?: boolean;
// Plug-and-play: Pass SLA data directly from module
slaData?: any; // SLAData | null - passed from module's business logic
isPaused?: boolean; // Pass pause status from module
} }
export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, onShareSummary, isInitiator }: RequestDetailHeaderProps) { export function RequestDetailHeader({
request,
refreshing,
onBack,
onRefresh,
onShareSummary,
isInitiator,
slaData, // Module passes prepared SLA data
isPaused = false // Module passes pause status
}: RequestDetailHeaderProps) {
const priorityConfig = getPriorityConfig(request?.priority || 'standard'); const priorityConfig = getPriorityConfig(request?.priority || 'standard');
const statusConfig = getStatusConfig(request?.status || 'pending'); const statusConfig = getStatusConfig(request?.status || 'pending');
const isPaused = request?.pauseInfo?.isPaused || false;
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">
@ -129,17 +140,19 @@ export function RequestDetailHeader({ request, refreshing, onBack, onRefresh, on
</div> </div>
</div> </div>
{/* SLA Progress Section */} {/* SLA Progress Section - Plug-and-play: Module passes prepared SLA data */}
{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={request.summary?.sla || request.sla} sla={slaData}
requestStatus={request.status} requestStatus={request.status}
isPaused={isPaused} isPaused={isPaused}
testId="request-sla" testId="request-sla"
/> />
</div> </div>
)}
</div> </div>
); );
} }