Merge branch 'development' of https://gitea.maskantech.in/gitea_admin/aeroresolve_frontend into waseem
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
MagnifyingGlassIcon,
|
||||
CalendarBlank,
|
||||
Export,
|
||||
CaretRight
|
||||
CaretRight,
|
||||
} from "@phosphor-icons/react";
|
||||
import {
|
||||
CustomTable,
|
||||
@@ -11,14 +11,11 @@ import {
|
||||
CustomStatus,
|
||||
Skeleton,
|
||||
CustomButton,
|
||||
CustomDropdown
|
||||
CustomDropdown,
|
||||
} from "../../../components/custom";
|
||||
import type { Column } from "../../../components/custom/CustomTable";
|
||||
import type { AuditLog, AuditLogFilters } from "../AuditLogsTypes";
|
||||
import {
|
||||
AUDIT_MODULE_LABELS,
|
||||
AUDIT_ACTION_VARIANTS,
|
||||
} from "../AuditLogsTypes";
|
||||
import { AUDIT_MODULE_LABELS, AUDIT_ACTION_VARIANTS } from "../AuditLogsTypes";
|
||||
import { getAuditLogs } from "../AuditLogsApi";
|
||||
import AuditLogDetail from "./AuditLogDetail";
|
||||
|
||||
@@ -32,9 +29,17 @@ function HeaderLabel({ text }: { text: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PrimaryText({ text, className = "" }: { text: string, className?: string }) {
|
||||
function PrimaryText({
|
||||
text,
|
||||
className = "",
|
||||
}: {
|
||||
text: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={`text-[13px] font-semibold text-[#0F172B] leading-[18px] ${className}`}>
|
||||
<div
|
||||
className={`text-[13px] font-semibold text-[#0F172B] leading-[18px] ${className}`}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
@@ -53,9 +58,11 @@ function ModuleBadge({ module }: { module: string }) {
|
||||
function getLogDescription(row: AuditLog) {
|
||||
const entity = row.entityLabel || row.entityId || "record";
|
||||
const mod = AUDIT_MODULE_LABELS[row.module] || row.module;
|
||||
if (row.action === "CREATE") return `New record created in ${mod}: ${entity}.`;
|
||||
if (row.action === "CREATE")
|
||||
return `New record created in ${mod}: ${entity}.`;
|
||||
if (row.action === "UPDATE") return `${mod} record updated: ${entity}.`;
|
||||
if (row.action === "STATUS_CHANGE") return `Status changed for ${entity} in ${mod}.`;
|
||||
if (row.action === "STATUS_CHANGE")
|
||||
return `Status changed for ${entity} in ${mod}.`;
|
||||
if (row.action === "DELETE") return `${mod} record deleted: ${entity}.`;
|
||||
return `Action ${row.action} performed on ${entity}.`;
|
||||
}
|
||||
@@ -88,33 +95,22 @@ export default function AuditLogsList() {
|
||||
if (daysFilter !== "all") {
|
||||
const now = new Date();
|
||||
const past = new Date();
|
||||
|
||||
|
||||
if (daysFilter === "today") {
|
||||
past.setHours(0, 0, 0, 0);
|
||||
} else {
|
||||
past.setDate(now.getDate() - parseInt(daysFilter, 10));
|
||||
}
|
||||
|
||||
|
||||
filters.dateFrom = past.toISOString();
|
||||
filters.dateTo = now.toISOString();
|
||||
}
|
||||
|
||||
const result = await getAuditLogs(filters);
|
||||
|
||||
// Client-side search on entityLabel / entityId
|
||||
const filtered = search
|
||||
? result.data.filter(
|
||||
(l) =>
|
||||
l.entityLabel?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.entityId?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.module?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.performedBy?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: result.data;
|
||||
|
||||
setLogs(filtered);
|
||||
setTotalItems(result.total);
|
||||
setTotalPages(result.totalPages);
|
||||
setLogs(result.data || []);
|
||||
setTotalItems(result.total || 0);
|
||||
setTotalPages(result.totalPages || 1);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch audit logs", err);
|
||||
setLogs([]);
|
||||
@@ -122,7 +118,7 @@ export default function AuditLogsList() {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[search, daysFilter]
|
||||
[daysFilter],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -137,20 +133,29 @@ export default function AuditLogsList() {
|
||||
const handleExportCSV = () => {
|
||||
if (logs.length === 0) return;
|
||||
|
||||
const headers = ["Timestamp", "User", "Action", "Module", "Description", "Audit ID"];
|
||||
const rows = logs.map(row => {
|
||||
const headers = [
|
||||
"Timestamp",
|
||||
"User",
|
||||
"Action",
|
||||
"Module",
|
||||
"Description",
|
||||
"Audit ID",
|
||||
];
|
||||
const rows = logs.map((row) => {
|
||||
const date = new Date(row.createdAt).toLocaleString();
|
||||
const user = row.performedBy ?? "System";
|
||||
const action = row.action;
|
||||
const module = AUDIT_MODULE_LABELS[row.module] ?? row.module;
|
||||
|
||||
|
||||
const entity = row.entityLabel || row.entityId || "record";
|
||||
let desc = `Action ${action} performed on ${entity}.`;
|
||||
if (action === "CREATE") desc = `New record created in ${module}: ${entity}.`;
|
||||
if (action === "CREATE")
|
||||
desc = `New record created in ${module}: ${entity}.`;
|
||||
if (action === "UPDATE") desc = `${module} record updated: ${entity}.`;
|
||||
if (action === "STATUS_CHANGE") desc = `Status changed for ${entity} in ${module}.`;
|
||||
if (action === "STATUS_CHANGE")
|
||||
desc = `Status changed for ${entity} in ${module}.`;
|
||||
if (action === "DELETE") desc = `${module} record deleted: ${entity}.`;
|
||||
|
||||
|
||||
const auditId = row.id;
|
||||
|
||||
// Escape quotes and wrap in quotes to handle commas in values
|
||||
@@ -160,14 +165,14 @@ export default function AuditLogsList() {
|
||||
`"${action.replace(/"/g, '""')}"`,
|
||||
`"${module.replace(/"/g, '""')}"`,
|
||||
`"${desc.replace(/"/g, '""')}"`,
|
||||
`"${auditId.replace(/"/g, '""')}"`
|
||||
`"${auditId.replace(/"/g, '""')}"`,
|
||||
].join(",");
|
||||
});
|
||||
|
||||
const csvContent = [headers.join(","), ...rows].join("\n");
|
||||
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `audit_logs_${new Date().getTime()}.csv`);
|
||||
@@ -183,11 +188,14 @@ export default function AuditLogsList() {
|
||||
header: <HeaderLabel text="Timestamp" />,
|
||||
accessor: (row) => {
|
||||
const d = new Date(row.createdAt);
|
||||
const time = d.toLocaleTimeString(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
}).toLowerCase().replace(' ', '');
|
||||
const time = d
|
||||
.toLocaleTimeString(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
.toLowerCase()
|
||||
.replace(" ", "");
|
||||
const date = d.toLocaleDateString(undefined, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
@@ -195,8 +203,12 @@ export default function AuditLogsList() {
|
||||
});
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[13px] font-medium text-[#6C766D]">{time}</span>
|
||||
<span className="text-[13px] font-medium text-[#6C766D]">{date}</span>
|
||||
<span className="text-[13px] font-medium text-[#6C766D]">
|
||||
{time}
|
||||
</span>
|
||||
<span className="text-[13px] font-medium text-[#6C766D]">
|
||||
{date}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -210,7 +222,8 @@ export default function AuditLogsList() {
|
||||
{
|
||||
header: <HeaderLabel text="Action" />,
|
||||
accessor: (row) => {
|
||||
const displayStatus = row.action === 'STATUS_CHANGE' ? 'Status Change' : row.action;
|
||||
const displayStatus =
|
||||
row.action === "STATUS_CHANGE" ? "Status Change" : row.action;
|
||||
return (
|
||||
<CustomStatus
|
||||
status={displayStatus}
|
||||
@@ -232,7 +245,7 @@ export default function AuditLogsList() {
|
||||
{
|
||||
header: <HeaderLabel text="Audit ID" />,
|
||||
accessor: (row) => {
|
||||
const hash = row.id.split('-')[0].substring(0, 4).toUpperCase();
|
||||
const hash = row.id.split("-")[0].substring(0, 4).toUpperCase();
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[13px] font-medium text-[#6C766D]">
|
||||
LOG-{hash}
|
||||
@@ -266,6 +279,15 @@ export default function AuditLogsList() {
|
||||
|
||||
// ─── Render ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const displayedLogs = search
|
||||
? logs.filter((l) =>
|
||||
l.entityLabel?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.entityId?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.module?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.performedBy?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: logs;
|
||||
|
||||
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
|
||||
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
|
||||
|
||||
@@ -273,14 +295,16 @@ export default function AuditLogsList() {
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
<CustomTable<AuditLog>
|
||||
columns={columns}
|
||||
data={logs}
|
||||
data={displayedLogs}
|
||||
leftHeaderActions={
|
||||
<div className="w-[360px]">
|
||||
<CustomInput
|
||||
placeholder="Search logs by keyword, user, or ID..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<MagnifyingGlassIcon size={16} className="text-[#6C766D]" />}
|
||||
leftIcon={
|
||||
<MagnifyingGlassIcon size={16} className="text-[#6C766D]" />
|
||||
}
|
||||
className="!bg-[#F8F9FA] !rounded-[8px] !h-[40px] !border-none !text-[13px]"
|
||||
containerClassName="!gap-0 border-none"
|
||||
/>
|
||||
@@ -292,7 +316,13 @@ export default function AuditLogsList() {
|
||||
<CustomDropdown
|
||||
value={daysFilter}
|
||||
onChange={setDaysFilter}
|
||||
leftIcon={<CalendarBlank size={16} weight="bold" className="text-[#1E8E3E]" />}
|
||||
leftIcon={
|
||||
<CalendarBlank
|
||||
size={16}
|
||||
weight="bold"
|
||||
className="text-[#1E8E3E]"
|
||||
/>
|
||||
}
|
||||
searchable={false}
|
||||
options={[
|
||||
{ label: "Today", value: "today" },
|
||||
@@ -304,7 +334,7 @@ export default function AuditLogsList() {
|
||||
className="!border-[#1E8E3E] !bg-[#E6F4EA] !h-[40px] hover:!bg-[#E6F4EA]"
|
||||
/>
|
||||
</div>
|
||||
<CustomButton
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={handleExportCSV}
|
||||
className="flex items-center gap-2 h-[40px] px-4 rounded-[8px] border border-[#1E8E3E] text-[#1E8E3E] text-[13px] font-bold hover:bg-[#E6F4EA] transition-colors cursor-pointer"
|
||||
@@ -327,10 +357,7 @@ export default function AuditLogsList() {
|
||||
}
|
||||
/>
|
||||
|
||||
<AuditLogDetail
|
||||
log={selectedLog}
|
||||
onClose={() => setSelectedLog(null)}
|
||||
/>
|
||||
<AuditLogDetail log={selectedLog} onClose={() => setSelectedLog(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function CohortList() {
|
||||
const [cohorts, setCohorts] = useState<CohartResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
// Server-side pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -113,8 +114,11 @@ export default function CohortList() {
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setDeleteLoading(true);
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
try {
|
||||
await deleteCohart(deleteTarget.id);
|
||||
setSuccessMsg(`Cohort "${deleteTarget.name}" deleted successfully.`);
|
||||
setDeleteTarget(null);
|
||||
fetchCohorts(currentPage);
|
||||
} catch {
|
||||
@@ -127,9 +131,12 @@ export default function CohortList() {
|
||||
const handleToggleStatus = async () => {
|
||||
if (!deactivateTarget) return;
|
||||
setStatusLoading(true);
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
try {
|
||||
const newStatus = deactivateTarget.status === 'Active' ? 'Inactive' : 'Active';
|
||||
await updateCohartStatus(deactivateTarget.id, { status: newStatus });
|
||||
setSuccessMsg(`Cohort "${deactivateTarget.name}" is now ${newStatus}.`);
|
||||
setDeactivateTarget(null);
|
||||
fetchCohorts(currentPage);
|
||||
} catch {
|
||||
@@ -239,6 +246,13 @@ export default function CohortList() {
|
||||
onClose={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
{successMsg && (
|
||||
<CustomAlertBanner
|
||||
message={successMsg}
|
||||
type="success"
|
||||
onClose={() => setSuccessMsg(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CustomTable<CohartResponse>
|
||||
columns={columns}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CustomSwitch,
|
||||
CustomCheckBox,
|
||||
CustomLoader,
|
||||
Skeleton,
|
||||
} from '../../../components/custom';
|
||||
import CustomSuccessModal from '../../../components/custom/CustomSuccessModal';
|
||||
import {
|
||||
@@ -649,7 +650,7 @@ export default function AddPolicyEngine() {
|
||||
jurisdictionId: Array.isArray(jurisdiction) && jurisdiction.length > 0 ? String(jurisdiction[0]) : undefined,
|
||||
jurisdictionIds: Array.isArray(jurisdiction) ? jurisdiction.map(String) : [],
|
||||
description: description || undefined,
|
||||
status: isDeploy ? 'active' : 'draft',
|
||||
status: isDeploy ? status.toLowerCase() : 'draft',
|
||||
audienceType: audienceType === 'Selected Cohorts' ? 'COHORT' : 'ALL',
|
||||
targetAudiences:
|
||||
audienceType === 'Selected Cohorts'
|
||||
@@ -713,13 +714,13 @@ export default function AddPolicyEngine() {
|
||||
valObj.booleanValue = !!val;
|
||||
} else if (fieldType === 'number' || fieldType === 'decimal' || fieldType === 'percentage') {
|
||||
valObj.numberValue = parseFloat(val) || 0;
|
||||
} else if (Array.isArray(val)) {
|
||||
valObj.textValue = JSON.stringify(val);
|
||||
} else if (fieldType === 'dropdown' || fieldType === 'select' || fieldDef?.lookupSource) {
|
||||
valObj.selectedValueId = String(val ?? '');
|
||||
const lookupOpts = fieldLookupOptionsMap[fieldDef?.lookupSource || ''] || [];
|
||||
const matchedOpt = lookupOpts.find((o) => o.value === val || o.id === val || o.code === val);
|
||||
valObj.textValue = matchedOpt ? matchedOpt.label : String(val ?? '');
|
||||
} else if (Array.isArray(val)) {
|
||||
valObj.textValue = JSON.stringify(val);
|
||||
} else {
|
||||
valObj.textValue = String(val ?? '');
|
||||
}
|
||||
@@ -875,8 +876,82 @@ export default function AddPolicyEngine() {
|
||||
|
||||
if (loadingPolicy) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-white">
|
||||
<CustomLoader label="Loading Policy Details..." />
|
||||
<div className="flex flex-col h-full bg-white min-h-screen">
|
||||
{/* ─── Header Skeleton ────────────────────────────────────────────── */}
|
||||
<div className="flex items-center justify-between py-4 bg-white border-b border-gray-200">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="mt-1 p-1">
|
||||
<Skeleton variant="circular" className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton className="w-48 h-7" />
|
||||
<Skeleton className="w-32 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── Body Content Skeleton ──────────────────────────────────────── */}
|
||||
<div className="flex-1 overflow-y-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden py-6 pb-32">
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
|
||||
{/* Policy Information Card */}
|
||||
<div className="bg-[#FAFAFA] rounded-[16px] p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Skeleton className="w-8 h-8 rounded-lg" />
|
||||
<Skeleton className="w-40 h-5" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
<Skeleton className="w-full h-11" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
<Skeleton className="w-full h-11" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
<Skeleton className="w-full h-11" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="w-24 h-4" />
|
||||
<Skeleton className="w-full h-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Audience Card */}
|
||||
<div className="bg-[#FAFAFA] rounded-[16px] p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Skeleton className="w-8 h-8 rounded-lg" />
|
||||
<Skeleton className="w-40 h-5" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center gap-8">
|
||||
<Skeleton className="w-32 h-6" />
|
||||
<Skeleton className="w-32 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rule Engine Card */}
|
||||
<div className="bg-[#FAFAFA] rounded-[16px] p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-8 h-8 rounded-lg" />
|
||||
<Skeleton className="w-32 h-5" />
|
||||
</div>
|
||||
<Skeleton className="w-40 h-10 rounded-lg" />
|
||||
</div>
|
||||
<div className="border border-gray-100 rounded-[12px] p-6 bg-white">
|
||||
<Skeleton className="w-full h-32" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1346,6 +1421,13 @@ export default function AddPolicyEngine() {
|
||||
value={fieldVal ?? ''}
|
||||
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)}
|
||||
placeholder={field.placeholder || 'Enter value...'}
|
||||
min={
|
||||
field.fieldType === 'date'
|
||||
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]
|
||||
: field.fieldType === 'datetime'
|
||||
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { PlusIcon, TrashIcon, MagnifyingGlassIcon, XIcon, PencilSimpleIcon, ChecksIcon } from '@phosphor-icons/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
MagnifyingGlassIcon,
|
||||
XIcon,
|
||||
PencilSimpleIcon,
|
||||
ChecksIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CustomTable,
|
||||
CustomInput,
|
||||
@@ -9,11 +16,16 @@ import {
|
||||
CustomActionMenu,
|
||||
CustomActionItem,
|
||||
CustomConfirmationModal,
|
||||
CustomAlertBanner,
|
||||
Skeleton,
|
||||
} from '../../../components/custom';
|
||||
import type { Column } from '../../../components/custom/CustomTable';
|
||||
import type { PolicyEngineResponse } from '../PolicyEngineTypes';
|
||||
import { getPolicies, deletePolicy, updatePolicyStatus } from '../PolicyEngineApi';
|
||||
} from "../../../components/custom";
|
||||
import type { Column } from "../../../components/custom/CustomTable";
|
||||
import type { PolicyEngineResponse } from "../PolicyEngineTypes";
|
||||
import {
|
||||
getPolicies,
|
||||
deletePolicy,
|
||||
updatePolicyStatus,
|
||||
} from "../PolicyEngineApi";
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,14 +35,16 @@ const PAGE_SIZE = 10;
|
||||
|
||||
function HeaderLabel({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="text-[14px] font-semibold text-[#6C766D] tracking-[0px]">{text}</span>
|
||||
<span className="text-[14px] font-semibold text-[#6C766D] tracking-[0px]">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CellText({ text }: { text: string | null | undefined }) {
|
||||
return (
|
||||
<span style={{ fontSize: '14px', color: '#676767', fontWeight: 500 }}>
|
||||
{text || '—'}
|
||||
<span style={{ fontSize: "14px", color: "#676767", fontWeight: 500 }}>
|
||||
{text || "—"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +63,8 @@ export default function PolicyEngineList() {
|
||||
const navigate = useNavigate();
|
||||
const [policies, setPolicies] = useState<PolicyEngineResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -56,36 +72,42 @@ export default function PolicyEngineList() {
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
// Filters
|
||||
const [search, setSearch] = useState('');
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Modal state
|
||||
const [deleteTarget, setDeleteTarget] = useState<PolicyEngineResponse | null>(null);
|
||||
const [deactivateTarget, setDeactivateTarget] = useState<PolicyEngineResponse | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<PolicyEngineResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [deactivateTarget, setDeactivateTarget] =
|
||||
useState<PolicyEngineResponse | null>(null);
|
||||
|
||||
// ─── Fetch data ─────────────────────────────────────────────
|
||||
|
||||
const fetchPolicies = useCallback((page: number) => {
|
||||
setLoading(true);
|
||||
getPolicies(page, PAGE_SIZE, search)
|
||||
.then((res) => {
|
||||
setPolicies(res.data || []);
|
||||
setTotalItems(res.total || 0);
|
||||
setTotalPages(res.totalPages || 1);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to fetch policies:', err);
|
||||
setPolicies([]);
|
||||
setTotalItems(0);
|
||||
setTotalPages(1);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [search]);
|
||||
const fetchPolicies = useCallback(
|
||||
(page: number) => {
|
||||
setLoading(true);
|
||||
getPolicies(page, PAGE_SIZE)
|
||||
.then((res) => {
|
||||
setPolicies(res.data || []);
|
||||
setTotalItems(res.total || 0);
|
||||
setTotalPages(res.totalPages || 1);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch policies:", err);
|
||||
setPolicies([]);
|
||||
setTotalItems(0);
|
||||
setTotalPages(1);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicies(currentPage);
|
||||
}, [currentPage, search, fetchPolicies]);
|
||||
}, [currentPage, fetchPolicies]);
|
||||
|
||||
// ─── Handlers ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -100,27 +122,43 @@ export default function PolicyEngineList() {
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
try {
|
||||
await deletePolicy(deleteTarget.id);
|
||||
setSuccessMsg(`Policy "${deleteTarget.policyName}" deleted successfully.`);
|
||||
setDeleteTarget(null);
|
||||
fetchPolicies(currentPage);
|
||||
} catch (err) {
|
||||
console.error('Failed to delete policy:', err);
|
||||
console.error("Failed to delete policy:", err);
|
||||
setError("Failed to delete policy. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async () => {
|
||||
if (!deactivateTarget) return;
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
try {
|
||||
const nextStatus = deactivateTarget.status === 'Active' ? 'inactive' : 'active';
|
||||
const nextStatus =
|
||||
deactivateTarget.status === "Active" ? "inactive" : "active";
|
||||
await updatePolicyStatus(deactivateTarget.id, nextStatus);
|
||||
setSuccessMsg(`Policy "${deactivateTarget.policyName}" is now ${nextStatus === 'active' ? 'Active' : 'Inactive'}.`);
|
||||
setDeactivateTarget(null);
|
||||
fetchPolicies(currentPage);
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle policy status:', err);
|
||||
console.error("Failed to toggle policy status:", err);
|
||||
setError("Failed to update policy status. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const displayedPolicies = search
|
||||
? policies.filter(p =>
|
||||
p.policyName?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.jurisdiction?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: policies;
|
||||
|
||||
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
|
||||
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
|
||||
|
||||
@@ -129,7 +167,7 @@ export default function PolicyEngineList() {
|
||||
const columns: Column<PolicyEngineResponse>[] = [
|
||||
{
|
||||
header: <HeaderLabel text="Policy Name" />,
|
||||
accessor: row => (
|
||||
accessor: (row) => (
|
||||
<span className="text-[13px] font-semibold text-[#0F172B] leading-[18px] tracking-[0px]">
|
||||
{row.policyName}
|
||||
</span>
|
||||
@@ -137,22 +175,22 @@ export default function PolicyEngineList() {
|
||||
},
|
||||
{
|
||||
header: <HeaderLabel text="Jurisdiction" />,
|
||||
accessor: row => <BadgeLabel text={row.jurisdiction} />,
|
||||
accessor: (row) => <BadgeLabel text={row.jurisdiction} />,
|
||||
},
|
||||
{
|
||||
header: <HeaderLabel text="Status" />,
|
||||
accessor: row => <CustomStatus status={row.status} />,
|
||||
accessor: (row) => <CustomStatus status={row.status} />,
|
||||
},
|
||||
{
|
||||
header: <HeaderLabel text="Last Modified" />,
|
||||
accessor: row => <CellText text={row.lastModified} />,
|
||||
accessor: (row) => <CellText text={row.lastModified} />,
|
||||
},
|
||||
{
|
||||
header: <HeaderLabel text="Action" />,
|
||||
className: 'text-right',
|
||||
accessor: row => (
|
||||
className: "text-right",
|
||||
accessor: (row) => (
|
||||
<CustomActionMenu>
|
||||
{row.status !== 'Active' && (
|
||||
{row.status !== "Active" && (
|
||||
<CustomActionItem
|
||||
icon={<ChecksIcon size={15} />}
|
||||
variant="success"
|
||||
@@ -161,7 +199,7 @@ export default function PolicyEngineList() {
|
||||
Activate
|
||||
</CustomActionItem>
|
||||
)}
|
||||
{row.status === 'Active' && (
|
||||
{row.status === "Active" && (
|
||||
<CustomActionItem
|
||||
icon={<XIcon size={15} />}
|
||||
onClick={() => setDeactivateTarget(row)}
|
||||
@@ -211,15 +249,29 @@ export default function PolicyEngineList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<CustomAlertBanner
|
||||
message={error}
|
||||
type="error"
|
||||
onClose={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
{successMsg && (
|
||||
<CustomAlertBanner
|
||||
message={successMsg}
|
||||
type="success"
|
||||
onClose={() => setSuccessMsg(null)}
|
||||
/>
|
||||
)}
|
||||
<CustomTable<PolicyEngineResponse>
|
||||
columns={columns}
|
||||
data={policies}
|
||||
data={displayedPolicies}
|
||||
leftHeaderActions={
|
||||
<div className="w-[380px]">
|
||||
<CustomInput
|
||||
placeholder="Search framework registry..."
|
||||
value={search}
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
leftIcon={<MagnifyingGlassIcon size={16} />}
|
||||
className="!bg-[#F3F6F5] !rounded-[10px] !h-[38px] !border !border-[#E5E7EB]"
|
||||
containerClassName="!gap-0"
|
||||
@@ -233,7 +285,7 @@ export default function PolicyEngineList() {
|
||||
size="md"
|
||||
leftIcon={<PlusIcon size={16} />}
|
||||
className="!rounded-[10px] !gap-[10px] !h-[40px] !bg-[#1E7D5C] hover:!bg-[#17664B]"
|
||||
onClick={() => navigate('/policy-engine/add')}
|
||||
onClick={() => navigate("/policy-engine/add")}
|
||||
>
|
||||
Deploy New Policy
|
||||
</CustomButton>
|
||||
@@ -265,13 +317,19 @@ export default function PolicyEngineList() {
|
||||
isOpen={!!deactivateTarget}
|
||||
onClose={() => setDeactivateTarget(null)}
|
||||
onConfirm={handleToggleStatus}
|
||||
title={deactivateTarget?.status === 'Active' ? 'Deactivate Policy' : 'Activate Policy'}
|
||||
title={
|
||||
deactivateTarget?.status === "Active"
|
||||
? "Deactivate Policy"
|
||||
: "Activate Policy"
|
||||
}
|
||||
description={
|
||||
deactivateTarget?.status === 'Active'
|
||||
deactivateTarget?.status === "Active"
|
||||
? `"${deactivateTarget?.policyName}" will be deactivated.`
|
||||
: `"${deactivateTarget?.policyName}" will be reactivated.`
|
||||
}
|
||||
confirmText={deactivateTarget?.status === 'Active' ? 'Deactivate' : 'Activate'}
|
||||
confirmText={
|
||||
deactivateTarget?.status === "Active" ? "Deactivate" : "Activate"
|
||||
}
|
||||
cancelText="Cancel"
|
||||
variant="warning"
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { RecoveryIncident } from '../RecoveryIncidentsTypes';
|
||||
interface AddRecoveryIncidentsProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: (createdCount: number, flightNumber: string, passengerName: string) => void;
|
||||
incident?: RecoveryIncident | null;
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ const DEFAULT_JURISDICTION_OPTIONS = [
|
||||
{ label: "CAA SG (Singapore)", value: "CAA_SG" },
|
||||
];
|
||||
|
||||
export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddRecoveryIncidentsProps) {
|
||||
export default function AddRecoveryIncidents({ isOpen, onClose, onSuccess, incident }: AddRecoveryIncidentsProps) {
|
||||
const [, setLoyaltyTierOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [jurisdictionOptions, setJurisdictionOptions] = useState<{ label: string; value: string }[]>(DEFAULT_JURISDICTION_OPTIONS);
|
||||
const [scenarioOptions, setScenarioOptions] = useState<{ label: string; value: string }[]>(DEFAULT_SCENARIO_OPTIONS);
|
||||
@@ -347,6 +348,14 @@ export default function AddRecoveryIncidents({ isOpen, onClose, incident }: AddR
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(
|
||||
selectedPassengers.length,
|
||||
formData.flightNumber || "TBD",
|
||||
selectedPassengers[0]?.passengerName || "Unknown"
|
||||
);
|
||||
}
|
||||
}
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
Skeleton,
|
||||
CustomActionMenu,
|
||||
CustomActionItem,
|
||||
CustomAlertBanner,
|
||||
CustomSuccessModal,
|
||||
} from "../../../components/custom";
|
||||
import type { Column } from "../../../components/custom/CustomTable";
|
||||
import type { RecoveryIncident, MetricCardData } from "../RecoveryIncidentsTypes";
|
||||
@@ -90,6 +92,8 @@ export default function RecoveryIncidentsList() {
|
||||
const [incidents, setIncidents] = useState<RecoveryIncident[]>([]);
|
||||
const [metrics, setMetrics] = useState<MetricCardData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getRecoveryMetrics()
|
||||
@@ -110,6 +114,14 @@ export default function RecoveryIncidentsList() {
|
||||
// Selection
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Success Modal
|
||||
const [successModalData, setSuccessModalData] = useState<{
|
||||
isOpen: boolean;
|
||||
count: number;
|
||||
flightNumber: string;
|
||||
passengerName: string;
|
||||
}>({ isOpen: false, count: 0, flightNumber: '', passengerName: '' });
|
||||
|
||||
// ─── Fetch data ─────────────────────────────────────────────
|
||||
|
||||
const fetchIncidents = useCallback(async () => {
|
||||
@@ -180,12 +192,16 @@ export default function RecoveryIncidentsList() {
|
||||
|
||||
const handleStatusChange = async (incident: RecoveryIncident, text: string) => {
|
||||
try {
|
||||
setError(null);
|
||||
setSuccessMsg(null);
|
||||
const { updateIncidentStatus, getRecoveryMetrics } = await import('../RecoveryIncidentsApi');
|
||||
await updateIncidentStatus(incident.id, text);
|
||||
fetchIncidents();
|
||||
getRecoveryMetrics().then((data) => setMetrics(data)).catch(() => { });
|
||||
setSuccessMsg(`Incident status updated to ${text} successfully.`);
|
||||
} catch (error) {
|
||||
console.error("Failed to update status", error);
|
||||
setError("Failed to update status. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -495,6 +511,20 @@ export default function RecoveryIncidentsList() {
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
{error && (
|
||||
<CustomAlertBanner
|
||||
message={error}
|
||||
type="error"
|
||||
onClose={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
{successMsg && (
|
||||
<CustomAlertBanner
|
||||
message={successMsg}
|
||||
type="success"
|
||||
onClose={() => setSuccessMsg(null)}
|
||||
/>
|
||||
)}
|
||||
{/* Metrics Row */}
|
||||
<div className="flex gap-4 w-full">
|
||||
{metrics.map((metric) => (
|
||||
@@ -575,6 +605,32 @@ export default function RecoveryIncidentsList() {
|
||||
setEditingIncident(null);
|
||||
fetchIncidents(); // Refresh list
|
||||
}}
|
||||
onSuccess={(createdCount, flightNumber, passengerName) => {
|
||||
setSuccessModalData({
|
||||
isOpen: true,
|
||||
count: createdCount,
|
||||
flightNumber,
|
||||
passengerName,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Success Modal */}
|
||||
<CustomSuccessModal
|
||||
isOpen={successModalData.isOpen}
|
||||
onClose={() => setSuccessModalData((prev) => ({ ...prev, isOpen: false }))}
|
||||
title={
|
||||
successModalData.count > 1
|
||||
? "Recovery Incidents Created."
|
||||
: "Recovery Incident Created."
|
||||
}
|
||||
label="FLIGHT & PASSENGER DETAILS"
|
||||
cohortName={`Flight ${successModalData.flightNumber}`}
|
||||
cohortDescription={
|
||||
successModalData.count > 1
|
||||
? `Successfully logged incidents for ${successModalData.count} passengers, including ${successModalData.passengerName}.`
|
||||
: `Successfully logged incident for ${successModalData.passengerName}.`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from "react";
|
||||
import { MagnifyingGlassIcon, CaretLeftIcon, CaretRightIcon, FunnelSimpleIcon, ArrowsDownUpIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
CaretLeftIcon,
|
||||
CaretRightIcon,
|
||||
FunnelSimpleIcon,
|
||||
ArrowsDownUpIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import CustomInput from "./CustomInput";
|
||||
|
||||
export interface Column<T> {
|
||||
@@ -53,7 +59,6 @@ export function CustomTable<T>({
|
||||
onRowClick,
|
||||
rowClassName,
|
||||
}: CustomTableProps<T>) {
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages && onPageChange) {
|
||||
onPageChange(newPage);
|
||||
@@ -61,16 +66,43 @@ export function CustomTable<T>({
|
||||
};
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
const pages: (number | string)[] = [];
|
||||
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 4) {
|
||||
pages.push(1, 2, 3, 4, 5, "...", totalPages);
|
||||
} else if (currentPage >= totalPages - 3) {
|
||||
pages.push(
|
||||
1,
|
||||
"...",
|
||||
totalPages - 4,
|
||||
totalPages - 3,
|
||||
totalPages - 2,
|
||||
totalPages - 1,
|
||||
totalPages,
|
||||
);
|
||||
} else {
|
||||
pages.push(
|
||||
1,
|
||||
"...",
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
"...",
|
||||
totalPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col bg-white rounded-[14px] shadow-sm border border-gray-100 overflow-hidden">
|
||||
|
||||
{/* Top Header Section */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
@@ -89,9 +121,7 @@ export function CustomTable<T>({
|
||||
{leftHeaderActions}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{rightHeaderActions}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">{rightHeaderActions}</div>
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
@@ -106,8 +136,18 @@ export function CustomTable<T>({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{col.header}
|
||||
{col.sortable && <ArrowsDownUpIcon size={14} className="cursor-pointer hover:text-gray-900" />}
|
||||
{col.filterable && <FunnelSimpleIcon size={14} className="cursor-pointer hover:text-gray-900" />}
|
||||
{col.sortable && (
|
||||
<ArrowsDownUpIcon
|
||||
size={14}
|
||||
className="cursor-pointer hover:text-gray-900"
|
||||
/>
|
||||
)}
|
||||
{col.filterable && (
|
||||
<FunnelSimpleIcon
|
||||
size={14}
|
||||
className="cursor-pointer hover:text-gray-900"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
@@ -122,7 +162,10 @@ export function CustomTable<T>({
|
||||
className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors ${onRowClick ? "cursor-pointer" : ""} ${rowClassName ? rowClassName(row) : ""}`}
|
||||
>
|
||||
{columns.map((col, colIndex) => (
|
||||
<td key={colIndex} className={`py-5 px-6 ${col.className || ''}`}>
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`py-5 px-6 ${col.className || ""}`}
|
||||
>
|
||||
{typeof col.accessor === "function"
|
||||
? col.accessor(row)
|
||||
: (row[col.accessor] as React.ReactNode)}
|
||||
@@ -132,7 +175,10 @@ export function CustomTable<T>({
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="py-12 text-center text-gray-500 text-sm">
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="py-12 text-center text-gray-500 text-sm"
|
||||
>
|
||||
No {itemName} found.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -144,7 +190,8 @@ export function CustomTable<T>({
|
||||
{/* Pagination Footer */}
|
||||
<div className="flex items-center justify-between py-4 px-6 bg-[#F3F6F5] border-t border-[#E4E9F2]">
|
||||
<div className="text-[13px] font-medium text-gray-500">
|
||||
Showing {totalItems > 0 ? startIndex : 0} to {endIndex} of {totalItems} {itemName}
|
||||
Showing {totalItems > 0 ? startIndex : 0} to {endIndex} of{" "}
|
||||
{totalItems} {itemName}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -157,20 +204,27 @@ export function CustomTable<T>({
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{getPageNumbers().map(page => (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => handlePageChange(page)}
|
||||
className={`w-8 h-8 flex items-center justify-center rounded-lg text-sm font-semibold transition-colors
|
||||
${currentPage === page
|
||||
? "bg-gradient-to-b from-primary to-primary-dark text-white"
|
||||
: "bg-white text-[#9FACA1] border border-[#9FACA1] hover:bg-gray-50 hover:text-gray-900"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
{getPageNumbers().map((page, index) =>
|
||||
page === "..." ? (
|
||||
<span key={`ellipsis-${index}`} className="px-2 text-gray-500">
|
||||
...
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handlePageChange(page as number)}
|
||||
className={`w-8 h-8 flex items-center justify-center rounded-lg text-sm font-semibold transition-colors
|
||||
${
|
||||
currentPage === page
|
||||
? "bg-[#1E7D5C] text-white border-none"
|
||||
: "bg-white text-[#9FACA1] border border-[#9FACA1] hover:bg-gray-50 hover:text-gray-900"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -24,7 +24,7 @@ import Skeleton from "./CustomSkeleton";
|
||||
import CustomTimePicker from "./CustomTimePicker";
|
||||
import CustomAccordionSection from "./CustomAccordionSection";
|
||||
import CustomFullModal from "./CustomFullModal";
|
||||
|
||||
import CustomSuccessModal from "./CustomSuccessModal";
|
||||
export {
|
||||
CustomInput,
|
||||
CustomTextArea,
|
||||
@@ -51,5 +51,6 @@ export {
|
||||
CustomAlertBanner,
|
||||
Skeleton,
|
||||
CustomTimePicker,
|
||||
CustomAccordionSection
|
||||
CustomAccordionSection,
|
||||
CustomSuccessModal
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user