request detil page enhanced
This commit is contained in:
parent
ea6cd5151b
commit
ecf2556c64
@ -27,8 +27,12 @@ export function SLAProgressBar({
|
||||
isPaused = false,
|
||||
testId = 'sla-progress'
|
||||
}: SLAProgressBarProps) {
|
||||
// Pure presentational component - no business logic
|
||||
// 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 (
|
||||
<div className="flex items-center gap-2" data-testid={`${testId}-status-only`}>
|
||||
{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
|
||||
// Green: 0-50%, Amber: 50-75%, Orange: 75-100%, Red: 100%+ (breached)
|
||||
// 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';
|
||||
|
||||
// Determine colors based on percentage (matching ApprovalStepCard logic)
|
||||
@ -117,12 +122,12 @@ export function SLAProgressBar({
|
||||
className={`text-xs ${colors.badge}`}
|
||||
data-testid={`${testId}-badge`}
|
||||
>
|
||||
{sla.percentageUsed || 0}% elapsed {isPaused && '(frozen)'}
|
||||
{percentageUsed}% elapsed {isPaused && '(frozen)'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={sla.percentageUsed || 0}
|
||||
value={percentageUsed}
|
||||
className="h-3 mb-2"
|
||||
indicatorClassName={colors.progress}
|
||||
data-testid={`${testId}-bar`}
|
||||
@ -130,7 +135,7 @@ export function SLAProgressBar({
|
||||
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-gray-600" data-testid={`${testId}-elapsed`}>
|
||||
{sla.elapsedText || formatHoursMinutes(sla.elapsedHours || 0)} elapsed
|
||||
{formatHoursMinutes(sla.elapsedHours || 0)} elapsed
|
||||
</span>
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
@ -146,7 +151,7 @@ export function SLAProgressBar({
|
||||
|
||||
{sla.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>
|
||||
)}
|
||||
|
||||
|
||||
@ -372,6 +372,9 @@ function CustomRequestDetailInner({ requestId: propRequestId, onBack, dynamicReq
|
||||
onRefresh={handleRefresh}
|
||||
onShareSummary={handleShareSummary}
|
||||
isInitiator={isInitiator}
|
||||
// Custom module: Business logic for preparing SLA data
|
||||
slaData={request?.summary?.sla || request?.sla || null}
|
||||
isPaused={request?.pauseInfo?.isPaused || false}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
|
||||
@ -101,7 +101,12 @@ export function ClaimManagementOverviewTab({
|
||||
return (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
{/* 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 */}
|
||||
<DealerInformationCard dealerInfo={claimRequest.dealerInfo} />
|
||||
|
||||
@ -9,8 +9,10 @@ import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TrendingUp, Clock, CheckCircle, CircleCheckBig, Upload, Mail, Download, Receipt, Activity } from 'lucide-react';
|
||||
import { formatDateTime } from '@/utils/dateFormatter';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
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 { InitiatorProposalApprovalModal } from './modals';
|
||||
import { DeptLeadIOApprovalModal } from './modals';
|
||||
@ -337,6 +339,17 @@ export function DealerClaimWorkflowTab({
|
||||
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 {
|
||||
step: step.step || index + 1,
|
||||
title: stepTitles[index] || `Step ${step.step || index + 1}`,
|
||||
@ -346,7 +359,7 @@ export function DealerClaimWorkflowTab({
|
||||
status: normalizedStatus as any,
|
||||
comment: step.comment || approval?.comment,
|
||||
approvedAt: step.approvedAt || approval?.timestamp,
|
||||
elapsedHours: step.elapsedHours,
|
||||
elapsedHours, // Only non-zero for active/completed steps
|
||||
ioDetails,
|
||||
dmsDetails,
|
||||
einvoiceUrl: step.step === 7 ? (approval as any)?.einvoiceUrl : undefined,
|
||||
@ -816,6 +829,19 @@ export function DealerClaimWorkflowTab({
|
||||
// Step is active if it's pending or in_progress and matches currentStep
|
||||
const isActive = (step.status === 'pending' || step.status === 'in_progress') && step.step === currentStep;
|
||||
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 (
|
||||
<div
|
||||
@ -871,10 +897,11 @@ export function DealerClaimWorkflowTab({
|
||||
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500">TAT: {step.tatHours}h</p>
|
||||
{step.elapsedHours && (
|
||||
<p className="text-xs text-gray-500">TAT: {formatHoursMinutes(step.tatHours)}</p>
|
||||
{/* 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">
|
||||
Elapsed: {step.elapsedHours}h
|
||||
Elapsed: {formatHoursMinutes(step.elapsedHours)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -887,6 +914,96 @@ export function DealerClaimWorkflowTab({
|
||||
</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 */}
|
||||
{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">
|
||||
|
||||
@ -7,13 +7,22 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Calendar, MapPin, DollarSign, Receipt } from 'lucide-react';
|
||||
import { ClaimActivityInfo } from '@/pages/RequestDetail/types/claimManagement.types';
|
||||
import { format } from 'date-fns';
|
||||
import { formatDateTime } from '@/utils/dateFormatter';
|
||||
|
||||
interface ActivityInformationCardProps {
|
||||
activityInfo: ClaimActivityInfo;
|
||||
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
|
||||
if (!activityInfo) {
|
||||
console.warn('[ActivityInformationCard] activityInfo is missing');
|
||||
@ -169,6 +178,24 @@ export function ActivityInformationCard({ activityInfo, className }: ActivityInf
|
||||
</p>
|
||||
</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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@ -410,6 +410,9 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
||||
onRefresh={handleRefresh}
|
||||
onShareSummary={handleShareSummary}
|
||||
isInitiator={isInitiator}
|
||||
// Dealer-claim module: Business logic for preparing SLA data
|
||||
slaData={request?.summary?.sla || request?.sla || null}
|
||||
isPaused={request?.pauseInfo?.isPaused || false}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
|
||||
@ -331,6 +331,9 @@ export function useRequestDetails(
|
||||
documents: mappedDocuments,
|
||||
spectators,
|
||||
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: {
|
||||
name: wf.initiator?.displayName || wf.initiator?.email,
|
||||
role: wf.initiator?.designation || undefined,
|
||||
|
||||
@ -15,12 +15,23 @@ interface RequestDetailHeaderProps {
|
||||
onRefresh: () => void;
|
||||
onShareSummary?: () => void;
|
||||
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 statusConfig = getStatusConfig(request?.status || 'pending');
|
||||
const isPaused = request?.pauseInfo?.isPaused || false;
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{/* SLA Progress Section */}
|
||||
<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'
|
||||
}`} data-testid="sla-section">
|
||||
<SLAProgressBar
|
||||
sla={request.summary?.sla || request.sla}
|
||||
requestStatus={request.status}
|
||||
isPaused={isPaused}
|
||||
testId="request-sla"
|
||||
/>
|
||||
</div>
|
||||
{/* 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 ${
|
||||
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">
|
||||
<SLAProgressBar
|
||||
sla={slaData}
|
||||
requestStatus={request.status}
|
||||
isPaused={isPaused}
|
||||
testId="request-sla"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user