feat: implement RecoveryPlanTab and update incident detail view with status management logic
This commit is contained in:
+4
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 608 B |
@@ -61,6 +61,10 @@ export function updateRecoveryIncident(id: string, data: Partial<RecoveryInciden
|
|||||||
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}`, data);
|
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateIncidentStatus(id: string, status: string): Promise<RecoveryIncident> {
|
||||||
|
return ApiClient.patch<any, RecoveryIncident>(`/recovery-incidents/${id}/status`, { status });
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteRecoveryIncident(id: string): Promise<void> {
|
export function deleteRecoveryIncident(id: string): Promise<void> {
|
||||||
return ApiClient.delete<any, void>(`/recovery-incidents/${id}`);
|
return ApiClient.delete<any, void>(`/recovery-incidents/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,11 +180,10 @@ export default function RecoveryIncidentsList() {
|
|||||||
|
|
||||||
const handleStatusChange = async (incident: RecoveryIncident, text: string) => {
|
const handleStatusChange = async (incident: RecoveryIncident, text: string) => {
|
||||||
try {
|
try {
|
||||||
const { updateRecoveryIncident } = await import('../RecoveryIncidentsApi');
|
const { updateIncidentStatus, getRecoveryMetrics } = await import('../RecoveryIncidentsApi');
|
||||||
await updateRecoveryIncident(incident.id, {
|
await updateIncidentStatus(incident.id, text);
|
||||||
status: text
|
|
||||||
});
|
|
||||||
fetchIncidents();
|
fetchIncidents();
|
||||||
|
getRecoveryMetrics().then((data) => setMetrics(data)).catch(() => {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update status", error);
|
console.error("Failed to update status", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,62 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
|
import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react';
|
||||||
import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom';
|
import { CustomButton, CustomTabs, CustomBackButton, CustomStatus } from '../../../components/custom';
|
||||||
import SummaryTab from './SummaryTab';
|
import SummaryTab from './SummaryTab';
|
||||||
import CaseDetailsTab from './CaseDetailsTab';
|
import CaseDetailsTab from './CaseDetailsTab';
|
||||||
import RecoveryPlanTab from './RecoveryPlanTab';
|
import RecoveryPlanTab from './RecoveryPlanTab';
|
||||||
import AuditTrailTab from './AuditTrailTab';
|
import AuditTrailTab from './AuditTrailTab';
|
||||||
import { SparkleIcon } from 'lucide-react';
|
import { SparkleIcon } from 'lucide-react';
|
||||||
import { getRecoveryIncident } from '../RecoveryIncidentsApi';
|
import { getRecoveryIncident, updateIncidentStatus } from '../RecoveryIncidentsApi';
|
||||||
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
|
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
|
||||||
|
|
||||||
|
function getStatusVariant(status?: string): "success" | "error" | "warning" | "info" | "neutral" {
|
||||||
|
if (!status) return "neutral";
|
||||||
|
const s = status.toLowerCase();
|
||||||
|
if (s.includes("appr") || s.includes("active") || s.includes("success")) return "success";
|
||||||
|
if (s.includes("reject") || s.includes("denied") || s.includes("error")) return "error";
|
||||||
|
if (s.includes("pend") || s.includes("review") || s.includes("warn")) return "warning";
|
||||||
|
if (s.includes("new") || s.includes("info")) return "info";
|
||||||
|
return "neutral";
|
||||||
|
}
|
||||||
|
|
||||||
export default function RecoveryIncidentTabs() {
|
export default function RecoveryIncidentTabs() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [activeTab, setActiveTab] = useState('Summary');
|
const [activeTab, setActiveTab] = useState('Summary');
|
||||||
const [incident, setIncident] = useState<RecoveryIncident | null>(null);
|
const [incident, setIncident] = useState<RecoveryIncident | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [updating, setUpdating] = useState(false);
|
||||||
|
|
||||||
|
const fetchIncident = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getRecoveryIncident(id);
|
||||||
|
setIncident(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load incident:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
fetchIncident();
|
||||||
setLoading(true);
|
|
||||||
getRecoveryIncident(id)
|
|
||||||
.then(data => {
|
|
||||||
setIncident(data);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Failed to load incident:", err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const handleStatusChange = async (newStatus: string) => {
|
||||||
|
if (!id || updating) return;
|
||||||
|
setUpdating(true);
|
||||||
|
try {
|
||||||
|
const updated = await updateIncidentStatus(id, newStatus);
|
||||||
|
setIncident(updated);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update incident status:", err);
|
||||||
|
} finally {
|
||||||
|
setUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
{
|
{
|
||||||
id: 'Summary',
|
id: 'Summary',
|
||||||
@@ -59,6 +84,8 @@ export default function RecoveryIncidentTabs() {
|
|||||||
return <div className="p-8 text-center text-gray-500">Loading incident...</div>;
|
return <div className="p-8 text-center text-gray-500">Loading incident...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentStatus = incident?.status || "Pending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col h-full relative">
|
<div className="w-full flex flex-col h-full relative">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -67,8 +94,11 @@ export default function RecoveryIncidentTabs() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CustomBackButton />
|
<CustomBackButton />
|
||||||
<h1 className="text-xl font-bold text-gray-900">{incident?.recoveryCode || id}</h1>
|
<h1 className="text-xl font-bold text-gray-900">{incident?.recoveryCode || id}</h1>
|
||||||
<CustomStatus status="UNDER REVIEW" variant="warning" className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide" />
|
<CustomStatus
|
||||||
<CustomStatus status="HIGH PRIORITY" variant="error" className="uppercase !text-[11px] !px-2.5 !py-1 !bg-red-100 !text-red-800 !tracking-wide" />
|
status={currentStatus}
|
||||||
|
variant={getStatusVariant(currentStatus)}
|
||||||
|
className="uppercase !text-[11px] !px-2.5 !py-1 !tracking-wide"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-sm text-gray-500 ml-8">
|
<div className="flex items-center gap-3 text-sm text-gray-500 ml-8">
|
||||||
<div className="flex items-center gap-1.5"><User size={16} /> {incident?.passengerName || 'Unknown'}</div>
|
<div className="flex items-center gap-1.5"><User size={16} /> {incident?.passengerName || 'Unknown'}</div>
|
||||||
@@ -87,12 +117,12 @@ export default function RecoveryIncidentTabs() {
|
|||||||
{/* Tabs Row */}
|
{/* Tabs Row */}
|
||||||
<div className="flex items-center justify-between py-6">
|
<div className="flex items-center justify-between py-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CustomTabs
|
<CustomTabs
|
||||||
tabs={tabItems}
|
tabs={tabItems}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
contentClassName="!hidden"
|
contentClassName="!hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -109,94 +139,100 @@ export default function RecoveryIncidentTabs() {
|
|||||||
<div className="flex gap-6 items-start pb-24">
|
<div className="flex gap-6 items-start pb-24">
|
||||||
{/* Left Column - Tab Content */}
|
{/* Left Column - Tab Content */}
|
||||||
<div className="flex-1 bg-[#F8F9FA] rounded-[24px] p-3 border border-gray-100">
|
<div className="flex-1 bg-[#F8F9FA] rounded-[24px] p-3 border border-gray-100">
|
||||||
<CustomTabs
|
<CustomTabs
|
||||||
tabs={tabItems}
|
tabs={tabItems}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
tabListClassName="!hidden"
|
tabListClassName="!hidden"
|
||||||
contentClassName="!mt-0"
|
contentClassName="!mt-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Sidebar */}
|
{/* Right Column - Sidebar */}
|
||||||
<div className="w-[360px] flex-shrink-0 bg-[#F8F9FA] rounded-[16px] border border-gray-100 relative">
|
<div className="w-[360px] flex-shrink-0 bg-[#F8F9FA] rounded-[16px] border border-gray-100 relative">
|
||||||
|
|
||||||
{/* Blur Overlay */}
|
{/* Blur Overlay */}
|
||||||
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/20 backdrop-blur-[3px] rounded-[16px]">
|
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/20 backdrop-blur-[3px] rounded-[16px]">
|
||||||
<h4 className="text-[18px] font-bold text-[#143d30] italic">"Coming soon"</h4>
|
<h4 className="text-[18px] font-bold text-[#143d30] italic">"Coming soon"</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar Content */}
|
{/* Sidebar Content */}
|
||||||
<div className="p-6 select-none pointer-events-none">
|
<div className="p-6 select-none pointer-events-none">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<div className="flex items-center gap-2 mb-6">
|
||||||
<span className="text-[#1B9869]"><SparkleIcon size={20} height="fill" /></span>
|
<span className="text-[#1B9869]"><SparkleIcon size={20} height="fill" /></span>
|
||||||
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider">AI RECOMMENDATION</h3>
|
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider">AI RECOMMENDATION</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex justify-between items-end mb-2">
|
<div className="flex justify-between items-end mb-2">
|
||||||
<span className="text-xs font-bold text-gray-400 tracking-wider">SATISFACTION PREDICT</span>
|
<span className="text-xs font-bold text-gray-400 tracking-wider">SATISFACTION PREDICT</span>
|
||||||
<span className="text-sm font-bold text-[#1B9869]">84%</span>
|
<span className="text-sm font-bold text-[#1B9869]">84%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
||||||
<div className="h-full bg-[#1B9869] w-[84%] rounded-full opacity-60"></div>
|
<div className="h-full bg-[#1B9869] w-[84%] rounded-full opacity-60"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-end mb-2">
|
<div className="flex justify-between items-end mb-2">
|
||||||
<span className="text-xs font-bold text-gray-400 tracking-wider">ESCALATION RISK</span>
|
<span className="text-xs font-bold text-gray-400 tracking-wider">ESCALATION RISK</span>
|
||||||
<span className="text-sm font-bold text-blue-500">12%</span>
|
<span className="text-sm font-bold text-blue-500">12%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
<div className="h-2 bg-white rounded-full overflow-hidden border border-gray-100">
|
||||||
<div className="h-full bg-blue-500 w-[12%] rounded-full opacity-60"></div>
|
<div className="h-full bg-blue-500 w-[12%] rounded-full opacity-60"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-6 border-t border-gray-200">
|
<div className="pt-6 border-t border-gray-200">
|
||||||
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider mb-4">NEXT RECOMMENDED ACTION</h3>
|
<h3 className="text-[13px] font-bold text-gray-400 tracking-wider mb-4">NEXT RECOMMENDED ACTION</h3>
|
||||||
<div className="bg-white rounded-xl p-5 mb-4 border border-gray-100 shadow-sm">
|
<div className="bg-white rounded-xl p-5 mb-4 border border-gray-100 shadow-sm">
|
||||||
<p className="text-sm text-gray-500 text-center">Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.</p>
|
<p className="text-sm text-gray-500 text-center">Approve the automated recovery payout of [250 EUR]. This will prevent a regulatory complaint and retain this high-value Platinum member.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
disabled
|
disabled
|
||||||
rightIcon={<ArrowLeftIcon size={16} weight="bold" className="rotate-180" />}
|
rightIcon={<ArrowLeftIcon size={16} weight="bold" className="rotate-180" />}
|
||||||
className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
|
className="w-full !py-3 !bg-[#1B9869]/50 !text-white !font-semibold !rounded-lg"
|
||||||
>
|
>
|
||||||
EXECUTE RECOMMENDATION
|
EXECUTE RECOMMENDATION
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Sticky Action Bar */}
|
{/* Bottom Sticky Action Bar */}
|
||||||
<div className="sticky bottom-[-24px] -mx-8 px-8 py-4 bg-white/90 backdrop-blur-md border-t border-gray-100 flex justify-between items-center z-10 mt-auto shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
|
<div className="sticky bottom-[-24px] -mx-8 px-8 py-4 bg-white/90 backdrop-blur-md border-t border-gray-100 flex justify-between items-center z-10 mt-auto shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
|
||||||
<div></div> {/* Spacer */}
|
<div></div> {/* Spacer */}
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant="text"
|
variant="text"
|
||||||
leftIcon={<X size={18} weight="bold" />}
|
disabled={updating}
|
||||||
className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100"
|
onClick={() => handleStatusChange("Rejected")}
|
||||||
>
|
leftIcon={<X size={18} weight="bold" />}
|
||||||
REJECT RECOVERY
|
className="!bg-[#FDE8E8] !text-[#E02424] !font-bold !rounded-lg !border !border-[#E02424] hover:!bg-red-100 disabled:opacity-50"
|
||||||
</CustomButton>
|
>
|
||||||
<CustomButton
|
REJECT RECOVERY
|
||||||
variant="text"
|
</CustomButton>
|
||||||
leftIcon={<ClockCounterClockwiseIcon size={18} weight="bold" />}
|
<CustomButton
|
||||||
className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A]"
|
variant="text"
|
||||||
>
|
disabled={updating}
|
||||||
MARK FOR REVIEW
|
onClick={() => handleStatusChange("Under Review")}
|
||||||
</CustomButton>
|
leftIcon={<ClockCounterClockwiseIcon size={18} weight="bold" />}
|
||||||
<CustomButton
|
className="!bg-[#FEF3C7] !text-[#B45309] !font-bold !rounded-lg !border !border-[#B45309] hover:!bg-[#FDE68A] disabled:opacity-50"
|
||||||
variant="primary"
|
>
|
||||||
leftIcon={<Checks size={18} weight="bold" />}
|
MARK FOR REVIEW
|
||||||
className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E]"
|
</CustomButton>
|
||||||
>
|
<CustomButton
|
||||||
APPROVE RECOVERY
|
variant="primary"
|
||||||
</CustomButton>
|
disabled={updating}
|
||||||
</div>
|
onClick={() => handleStatusChange("Approved")}
|
||||||
|
leftIcon={<Checks size={18} weight="bold" />}
|
||||||
|
className="!bg-[#1B9869] !bg-none !text-white !font-bold !rounded-lg !border !border-[#1B9869] hover:!bg-[#14704E] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
APPROVE RECOVERY
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user