feat: implement recovery incident management with creation types and UI components
This commit is contained in:
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user