144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { ClipboardTextIcon, CheckCircleIcon } from '@phosphor-icons/react';
|
|
import { formatDate } from '../../../utils/formatDate';
|
|
import { getIncidentAuditTrail, type AuditTrailStepDto } from '../RecoveryIncidentsApi';
|
|
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
|
|
|
|
interface AuditTrailTabProps {
|
|
incident?: RecoveryIncident | null;
|
|
}
|
|
|
|
export default function AuditTrailTab({ incident }: AuditTrailTabProps) {
|
|
const [apiSteps, setApiSteps] = useState<AuditTrailStepDto[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const code = incident?.recoveryCode || 'REC-INCIDENT';
|
|
const passenger = incident?.passengerName || 'Passenger';
|
|
const flight = incident?.flightNumber || 'Flight';
|
|
const route = incident?.flightRoute || 'Route';
|
|
const status = incident?.status || 'Pending';
|
|
const evaluation = incident?.evaluation;
|
|
const policyName = evaluation?.policyName || 'Standard Policy';
|
|
const cohortName = evaluation?.matchedCohortName || 'General Audience';
|
|
const recoveryScore = evaluation?.recoveryScore || 0;
|
|
|
|
useEffect(() => {
|
|
if (!incident?.id) return;
|
|
setLoading(true);
|
|
getIncidentAuditTrail(incident.id)
|
|
.then((data) => {
|
|
if (Array.isArray(data)) {
|
|
setApiSteps(data);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('Failed to load backend audit trail:', err);
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}, [incident?.id, incident?.status, incident?.evaluation?.updatedAt]);
|
|
|
|
const checkIcon = <CheckCircleIcon size={24} weight="fill" className="text-[#1B9869]" />;
|
|
|
|
const isApproved = status.toLowerCase().includes('appr');
|
|
const isRejected = status.toLowerCase().includes('reject');
|
|
|
|
// Initial default steps if API response is loading
|
|
const defaultInitialSteps: AuditTrailStepDto[] = [
|
|
{
|
|
id: 'step-1',
|
|
title: 'Flight Disruption Recorded',
|
|
description: `Disruption identified for ${passenger} on flight ${flight} (${route}).`,
|
|
timestamp: incident?.createdAt || incident?.date || new Date().toISOString(),
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'step-2',
|
|
title: 'Target Audience Cohort Evaluated',
|
|
description: `Automated audience eligibility assessment performed against active frameworks. Matched cohort: "${cohortName}".`,
|
|
timestamp: incident?.createdAt || new Date().toISOString(),
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'step-3',
|
|
title: 'Policy Evaluated',
|
|
description: `Evaluated against active rules under "${policyName}". Recovery Score: ${recoveryScore}/100.`,
|
|
timestamp: evaluation?.createdAt || incident?.createdAt || new Date().toISOString(),
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'step-4',
|
|
title: `Status: ${status}`,
|
|
description: `Case officer assigned. Case status updated to "${status}".`,
|
|
timestamp: incident?.updatedAt || new Date().toISOString(),
|
|
status: 'completed',
|
|
},
|
|
{
|
|
id: 'step-5',
|
|
title: 'Recovery Resolution',
|
|
description: isApproved
|
|
? 'Recovery incident approved. Automated settlement and customer notification dispatched.'
|
|
: isRejected
|
|
? 'Recovery incident rejected by case officer.'
|
|
: 'Refund and compensation settlement will initiate upon final approval.',
|
|
timestamp: incident?.updatedAt || new Date().toISOString(),
|
|
status: isApproved ? 'completed' : isRejected ? 'rejected' : 'pending',
|
|
},
|
|
];
|
|
|
|
const displaySteps = apiSteps.length > 0 ? apiSteps : defaultInitialSteps;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="bg-white rounded-2xl p-6 border border-gray-100 shadow-sm flex flex-col gap-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<ClipboardTextIcon size={20} weight="bold" className="text-[#143d30]" />
|
|
<h3 className="text-[13px] font-bold text-[#143d30] tracking-wider uppercase">
|
|
Lifecycle Timeline & Audit Trail ({code})
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col mt-4 pl-2">
|
|
{displaySteps.map((step, idx) => {
|
|
const isLast = idx === displaySteps.length - 1;
|
|
const rawTime = (step as any).createdAt || step.timestamp;
|
|
|
|
return (
|
|
<div key={step.id || idx} className={`relative pl-10 ${isLast ? '' : 'pb-8'}`}>
|
|
{/* Connecting Line */}
|
|
{!isLast && (
|
|
<div
|
|
className="absolute left-[7px] top-7 bottom-0 w-[2px] bg-[#1B9869]"
|
|
></div>
|
|
)}
|
|
|
|
{/* Node Icon Indicator */}
|
|
<div className="absolute left-[-4px] top-0.5 bg-white flex items-center justify-center">
|
|
<div className="text-[#1B9869]">{checkIcon}</div>
|
|
</div>
|
|
|
|
{/* Step Details */}
|
|
<div className="flex justify-between items-start gap-4">
|
|
<div className="flex flex-col gap-1 max-w-xl">
|
|
<h4 className="text-sm font-semibold text-gray-900">{step.title}</h4>
|
|
<p className="text-[13px] text-gray-500 leading-relaxed">
|
|
{step.description}
|
|
</p>
|
|
</div>
|
|
|
|
<span className="text-[11px] font-medium text-gray-400 tracking-wider shrink-0">
|
|
{formatDate(rawTime)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|