feat: implement recovery incident management with creation types and UI components

This commit is contained in:
azeeee05
2026-08-06 11:48:07 +05:30
parent ba2535d4c7
commit 35a6f81a90
3 changed files with 136 additions and 16 deletions
@@ -14,5 +14,6 @@ export interface RecoveryIncident {
category?: string;
statuses: IncidentStatus[];
value: string;
isPerksClaimed?: boolean;
isGroupHeader?: boolean;
}
@@ -1,21 +1,24 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { FileText, User, Airplane, WarningCircle, CaretRight } from '@phosphor-icons/react';
import {
CustomModal,
CustomInput,
CustomDropdown,
CustomCheckBox,
} from "../../../components/custom";
import { createRecoveryIncident } from '../RecoveryIncidentsApi';
import { createRecoveryIncident, updateRecoveryIncident } from '../RecoveryIncidentsApi';
import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
interface AddRecoveryIncidentsProps {
isOpen: boolean;
onClose: () => void;
incident?: RecoveryIncident | null;
}
const SECTION_TITLE_CLASS = "flex items-center gap-2 mb-4 text-[#4A5568] font-bold text-xs tracking-wider uppercase";
const SECTION_CONTAINER_CLASS = "bg-[#F9FAFB] rounded-[14px] p-5 border border-gray-100";
export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryIncidentsProps) {
export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddRecoveryIncidentsProps) {
const [formData, setFormData] = useState({
passengerName: "",
pnr: "",
@@ -28,35 +31,80 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
scenario: "",
jurisdiction: "",
delayDuration: "",
isPerksClaimed: false,
});
useEffect(() => {
if (incident && isOpen) {
const [origin, destination] = incident.flightRoute ? incident.flightRoute.split(' → ') : ["", ""];
setFormData({
passengerName: incident.passengerName || "",
pnr: incident.pnr || "",
loyaltyTier: "",
flightNumber: incident.flightNumber || "",
date: incident.date ? incident.date.split('T')[0] : "",
origin: origin?.trim() || "",
destination: destination?.trim() || "",
category: incident.category || "",
scenario: "",
jurisdiction: "",
delayDuration: "",
isPerksClaimed: incident.isPerksClaimed || false,
});
} else if (isOpen) {
setFormData({
passengerName: "",
pnr: "",
loyaltyTier: "",
flightNumber: "",
date: "",
origin: "",
destination: "",
category: "",
scenario: "",
jurisdiction: "",
delayDuration: "",
isPerksClaimed: false,
});
}
}, [incident, isOpen]);
const [loading, setLoading] = useState(false);
const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const handleCheckboxChange = (field: string, checked: boolean) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const handleSubmit = async () => {
setLoading(true);
try {
const payload = {
recoveryCode: "REC-" + Math.floor(Math.random() * 10000),
recoveryCode: incident ? incident.recoveryCode : "REC-" + Math.floor(Math.random() * 10000),
passengerName: formData.passengerName || "Unknown",
pnr: formData.pnr || "N/A",
flightNumber: formData.flightNumber || "TBD",
flightRoute: `${formData.origin || 'UNK'}${formData.destination || 'UNK'}`,
date: formData.date ? new Date(formData.date).toISOString() : new Date().toISOString(),
category: formData.category || "General",
statuses: [{ text: "New", variant: "info" as const }],
value: "$0",
statuses: incident ? incident.statuses : [{ text: "New", variant: "info" as const }],
value: incident ? incident.value : "$0",
isPerksClaimed: formData.isPerksClaimed,
};
console.log("Submitting payload:", payload);
await createRecoveryIncident(payload);
if (incident) {
await updateRecoveryIncident(incident.id, payload);
} else {
await createRecoveryIncident(payload);
}
onClose(); // Will trigger refresh in parent list
} catch (error: any) {
console.error("Error creating incident:", error.response?.data || error.message || error);
alert(`Failed to add incident: ${error.response?.data?.message || 'Unknown error'}`);
console.error("Error saving incident:", error.response?.data || error.message || error);
alert(`Failed to save incident: ${error.response?.data?.message || 'Unknown error'}`);
} finally {
setLoading(false);
}
@@ -66,12 +114,12 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
<CustomModal
isOpen={isOpen}
onClose={onClose}
title="New Recovery Incident"
title={incident ? "Edit Recovery Incident" : "New Recovery Incident"}
description="Log a disruption case and assess against policy frameworks"
icon={<FileText className="text-[#1B9869]" />}
size="lg"
primaryAction={{
label: loading ? "Logging..." : "Assess & Log Incident",
label: loading ? "Saving..." : (incident ? "Save Changes" : "Assess & Log Incident"),
onClick: handleSubmit,
icon: <CaretRight size={16} />
}}
@@ -211,6 +259,13 @@ export default function AddRecoveryIncidents({ isOpen, onClose }: AddRecoveryInc
onChange={(e) => handleInputChange("delayDuration", e.target.value)}
/>
</div>
<div className="mt-4">
<CustomCheckBox
label="Passenger claimed the perks"
checked={formData.isPerksClaimed}
onChange={(e) => handleCheckboxChange("isPerksClaimed", e.target.checked)}
/>
</div>
</div>
</div>
</CustomModal>
@@ -5,9 +5,13 @@ import {
MagnifyingGlass,
CaretUp,
CaretDown,
DotsThreeVertical,
FadersHorizontal,
SquaresFour,
Eye,
PencilSimple,
CheckCircle,
XCircle,
Clock,
} from "@phosphor-icons/react";
import {
CustomTable,
@@ -16,6 +20,8 @@ import {
CustomStatus,
CustomCheckBox,
Skeleton,
CustomActionMenu,
CustomActionItem,
} from "../../../components/custom";
import type { Column } from "../../../components/custom/CustomTable";
import type { RecoveryIncident } from "../RecoveryIncidentsTypes";
@@ -172,6 +178,7 @@ export default function RecoveryIncidentsList() {
const [isGrouped, setIsGrouped] = useState(true);
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingIncident, setEditingIncident] = useState<RecoveryIncident | null>(null);
// Selection
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -256,6 +263,18 @@ export default function RecoveryIncidentsList() {
});
};
const handleStatusChange = async (incident: RecoveryIncident, text: string, variant: "success" | "error" | "warning" | "info" | "neutral" | "brand") => {
try {
const { updateRecoveryIncident } = await import('../RecoveryIncidentsApi');
await updateRecoveryIncident(incident.id, {
statuses: [{ text, variant }]
});
fetchIncidents(currentPage);
} catch (error) {
console.error("Failed to update status", error);
}
};
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
@@ -353,6 +372,13 @@ export default function RecoveryIncidentsList() {
),
accessor: (row) => <PrimaryText text={row.value} />,
},
{
header: <HeaderLabel text="Perks Claimed" />,
accessor: (row) =>
row.isGroupHeader ? null : (
<BadgeLabel text={row.isPerksClaimed ? "Yes" : "No"} />
),
},
{
header: <HeaderLabel text="Action" />,
className: "text-right",
@@ -370,9 +396,42 @@ export default function RecoveryIncidentsList() {
)}
</div>
) : (
<div className="cursor-pointer text-gray-500 hover:text-gray-900 transition-colors p-1">
<DotsThreeVertical size={20} />
</div>
<CustomActionMenu>
<CustomActionItem
onClick={() => navigate(`/recovery/${row.id}`)}
icon={<Eye size={16} className="text-blue-500" />}
>
View Details
</CustomActionItem>
<CustomActionItem
onClick={() => {
setEditingIncident(row);
setIsModalOpen(true);
}}
icon={<PencilSimple size={16} className="text-yellow-500" />}
>
Edit Incident
</CustomActionItem>
<CustomActionItem
icon={<CheckCircle size={16} className="text-green-500" />}
onClick={() => handleStatusChange(row, "Approved", "success")}
>
Approve
</CustomActionItem>
<CustomActionItem
variant="danger"
icon={<XCircle size={16} className="text-red-500" />}
onClick={() => handleStatusChange(row, "Rejected", "error")}
>
Reject
</CustomActionItem>
<CustomActionItem
icon={<Clock size={16} className="text-yellow-600" />}
onClick={() => handleStatusChange(row, "Under Review", "warning")}
>
Mark for Review
</CustomActionItem>
</CustomActionMenu>
)}
</div>
),
@@ -480,7 +539,10 @@ export default function RecoveryIncidentsList() {
size="md"
leftIcon={<Plus size={16} />}
className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
onClick={() => setIsModalOpen(true)}
onClick={() => {
setEditingIncident(null);
setIsModalOpen(true);
}}
>
New Incident
</CustomButton>
@@ -500,8 +562,10 @@ export default function RecoveryIncidentsList() {
{/* Modal */}
<AddRecoveryIncidents
isOpen={isModalOpen}
incident={editingIncident}
onClose={() => {
setIsModalOpen(false);
setEditingIncident(null);
fetchIncidents(currentPage); // Refresh list
}}
/>