Policy engine screens

This commit is contained in:
azeeee05
2026-07-13 11:12:54 +05:30
parent 194070ea7f
commit b518312696
6 changed files with 927 additions and 1 deletions
+4
View File
@@ -2,6 +2,8 @@ import { Route, Routes } from 'react-router-dom'
import Layout from './layout/AppLayout'
import HomePage from './app/dashboard'
import CohortManage from './app/cohartManage'
import PolicyEngineList from './app/policyEngine/components/PolicyEngineList'
import AddPolicyEngine from './app/policyEngine/components/AddPolicyEngine'
function AppRoutes() {
return (
@@ -9,6 +11,8 @@ function AppRoutes() {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/cohorts" element={<CohortManage />} />
<Route path="/policy-engine" element={<PolicyEngineList />} />
<Route path="/policy-engine/add" element={<AddPolicyEngine />} />
</Routes>
</Layout>
)
+14
View File
@@ -0,0 +1,14 @@
export interface PolicyEngineResponse {
id: string;
policyName: string;
jurisdiction: string;
status: 'Active' | 'Inactive' | 'Draft';
lastModified: string;
}
export interface PaginatedPolicyEngineResponse {
data: PolicyEngineResponse[];
total: number;
totalPages: number;
page: number;
}
@@ -0,0 +1,594 @@
import { useState } from 'react';
import {
ArrowLeft, Plus, Trash2, Minus, Bell, Book, Users, GitBranch
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
CustomInput,
CustomDropdown,
CustomButton,
CustomTextArea,
CustomStatus
} from '../../../components/custom';
// ─── Types ───────────────────────────────────────────────────────────────────
type Condition = {
id: string;
condition: string;
operator: string;
value: string;
logic: string;
};
type Action = {
id: string;
actionType: string;
roomTypeTier: string;
duration: string;
logic: string;
};
type Rule = {
id: string;
category: string;
priority: number;
conditions: Condition[];
actions: Action[];
};
// ─── Constants ───────────────────────────────────────────────────────────────
const JURISDICTION_OPTIONS = [
{ label: 'GLOBAL', value: 'GLOBAL' },
{ label: 'US', value: 'US' },
{ label: 'EU', value: 'EU' },
];
const COHORT_OPTIONS = [
{ label: 'Premium Members', value: 'Premium Members' },
{ label: 'Frequent Flyers', value: 'Frequent Flyers' },
{ label: 'Families', value: 'Families' },
];
const RULE_CATEGORY_OPTIONS = [
{ label: 'Compensation', value: 'Compensation' },
{ label: 'Accommodation', value: 'Accommodation' },
{ label: 'Rebooking', value: 'Rebooking' },
];
const CONDITION_OPTIONS = [
{ label: 'Delay Duration', value: 'Delay Duration' },
{ label: 'Flight Distance', value: 'Flight Distance' },
{ label: 'Passenger Tier', value: 'Passenger Tier' },
];
const OPERATOR_OPTIONS = [
{ label: 'Greater Than', value: 'Greater Than' },
{ label: 'Less Than', value: 'Less Than' },
{ label: 'Equals', value: 'Equals' },
];
const ACTION_TYPE_OPTIONS = [
{ label: 'Hotel Booking', value: 'Hotel Booking' },
{ label: 'Voucher', value: 'Voucher' },
{ label: 'Lounge Access', value: 'Lounge Access' },
];
const ROOM_TYPE_TIER_OPTIONS = [
{ label: 'Standard', value: 'Standard' },
{ label: 'Premium', value: 'Premium' },
{ label: 'Suite', value: 'Suite' },
];
const LOGIC_OPTIONS = [
{ label: 'AND', value: 'AND' },
{ label: 'OR', value: 'OR' },
];
// ─── Component ───────────────────────────────────────────────────────────────
export default function AddPolicyEngine() {
const navigate = useNavigate();
// Policy Info State
const [policyName, setPolicyName] = useState('');
const [jurisdiction, setJurisdiction] = useState('');
const [status, setStatus] = useState<'Active' | 'Inactive'>('Active');
const [description, setDescription] = useState('');
// Target Audience State
const [audienceType, setAudienceType] = useState<'All Passengers' | 'Selected Cohorts'>('All Passengers');
const [selectedCohort, setSelectedCohort] = useState('');
// Rule Engine State
const [rules, setRules] = useState<Rule[]>([
{
id: crypto.randomUUID(),
category: '',
priority: 2,
conditions: [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }],
actions: [{ id: crypto.randomUUID(), actionType: '', roomTypeTier: '', duration: '', logic: 'AND' }]
}
]);
// ─── Handlers ──────────────────────────────────────────────────────────────
const handleAddRule = () => {
setRules([...rules, {
id: crypto.randomUUID(),
category: '',
priority: 1,
conditions: [{ id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }],
actions: [{ id: crypto.randomUUID(), actionType: '', roomTypeTier: '', duration: '', logic: 'AND' }]
}]);
};
const handleDeleteRule = (ruleId: string) => {
setRules(rules.filter(r => r.id !== ruleId));
};
const handleUpdateRule = (ruleId: string, updates: Partial<Rule>) => {
setRules(rules.map(r => r.id === ruleId ? { ...r, ...updates } : r));
};
const handleAddCondition = (ruleId: string) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return {
...r,
conditions: [...r.conditions, { id: crypto.randomUUID(), condition: '', operator: '', value: '', logic: 'AND' }]
};
}
return r;
}));
};
const handleUpdateCondition = (ruleId: string, conditionId: string, updates: Partial<Condition>) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return {
...r,
conditions: r.conditions.map(c => c.id === conditionId ? { ...c, ...updates } : c)
};
}
return r;
}));
};
const handleDeleteCondition = (ruleId: string, conditionId: string) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return { ...r, conditions: r.conditions.filter(c => c.id !== conditionId) };
}
return r;
}));
};
const handleAddAction = (ruleId: string) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return {
...r,
actions: [...r.actions, { id: crypto.randomUUID(), actionType: '', roomTypeTier: '', duration: '', logic: 'AND' }]
};
}
return r;
}));
};
const handleUpdateAction = (ruleId: string, actionId: string, updates: Partial<Action>) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return {
...r,
actions: r.actions.map(a => a.id === actionId ? { ...a, ...updates } : a)
};
}
return r;
}));
};
const handleDeleteAction = (ruleId: string, actionId: string) => {
setRules(rules.map(r => {
if (r.id === ruleId) {
return { ...r, actions: r.actions.filter(a => a.id !== actionId) };
}
return r;
}));
};
// ─── Render Helpers ────────────────────────────────────────────────────────
const CardHeader = ({ icon: Icon, title }: { icon: any, title: string }) => (
<div className="flex items-center gap-2 mb-5">
<div className="w-8 h-8 rounded-lg bg-[#E8F3EF] flex items-center justify-center text-[#1E7D5C]">
<Icon size={18} />
</div>
<h2 className="text-base font-bold text-[#0F172B]">{title}</h2>
</div>
);
return (
<div className="flex flex-col h-full bg-[#FAFAFA] min-h-screen">
{/* ─── Header ────────────────────────────────────────────────────── */}
<div className="flex items-center justify-between px-8 py-4 bg-white border-b border-gray-200">
<div className="flex items-start gap-4">
<button
onClick={() => navigate('/policy-engine')}
className="mt-1 p-1 hover:bg-gray-100 rounded-full transition-colors text-gray-500"
>
<ArrowLeft size={20} />
</button>
<div className="flex flex-col">
<h1 className="text-xl font-bold text-[#0F172B]">Deploy New Policy</h1>
<span className="text-[13px] text-gray-500">Global Framework Registry</span>
</div>
</div>
<div className="flex items-center gap-3">
<button className="p-2 text-gray-400 hover:text-gray-600 transition-colors">
<Bell size={20} />
</button>
<CustomButton
variant="primary"
className="!bg-[#1E7D5C] hover:!bg-[#17664B] !h-10 !px-4 !rounded-[10px]"
leftIcon={<Plus size={16} />}
>
Deploy Policy
</CustomButton>
</div>
</div>
{/* ─── Body Content ──────────────────────────────────────────────── */}
<div className="flex-1 overflow-y-auto px-8 py-6 pb-32">
<div className="max-w-[1200px] mx-auto flex flex-col gap-6">
{/* Policy Information Card */}
<div className="bg-white rounded-[16px] p-6 shadow-sm border border-gray-100">
<CardHeader icon={Book} title="Policy Information" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Policy Name<span className="text-red-500">*</span></label>
<CustomInput
value={policyName}
onChange={(e) => setPolicyName(e.target.value)}
placeholder="Selected Option"
className="!h-11"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Jurisdiction</label>
<CustomDropdown
options={JURISDICTION_OPTIONS}
value={jurisdiction}
onChange={setJurisdiction}
placeholder="Selected Option"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Status</label>
<div className="flex items-center gap-6 h-11">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="status"
checked={status === 'Active'}
onChange={() => setStatus('Active')}
className="w-4 h-4 text-[#1E7D5C] focus:ring-[#1E7D5C]"
/>
<span className="text-[14px] text-gray-700">Active</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="status"
checked={status === 'Inactive'}
onChange={() => setStatus('Inactive')}
className="w-4 h-4 text-[#1E7D5C] focus:ring-[#1E7D5C]"
/>
<span className="text-[14px] text-gray-700">Inactive</span>
</label>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Description</label>
<CustomTextArea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description..."
className="!h-24 resize-none"
/>
</div>
</div>
{/* Target Audience Card */}
<div className="bg-white rounded-[16px] p-6 shadow-sm border border-gray-100">
<CardHeader icon={Users} title="Target Audience" />
<div className="flex flex-col gap-6">
<div className="flex items-center gap-8">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="audience"
checked={audienceType === 'All Passengers'}
onChange={() => setAudienceType('All Passengers')}
className="w-4 h-4 text-[#1E7D5C] focus:ring-[#1E7D5C]"
/>
<span className="text-[14px] font-medium text-gray-700">All Passengers</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="audience"
checked={audienceType === 'Selected Cohorts'}
onChange={() => setAudienceType('Selected Cohorts')}
className="w-4 h-4 text-[#1E7D5C] focus:ring-[#1E7D5C]"
/>
<span className="text-[14px] font-medium text-gray-700">Selected Cohorts</span>
</label>
</div>
{audienceType === 'Selected Cohorts' && (
<div className="w-1/3 flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Cohort</label>
<CustomDropdown
options={COHORT_OPTIONS}
value={selectedCohort}
onChange={setSelectedCohort}
placeholder="Selected Option"
/>
</div>
)}
</div>
</div>
{/* Rule Engine Card */}
<div className="bg-white rounded-[16px] p-6 shadow-sm border border-gray-100">
<div className="flex items-center justify-between mb-6">
<CardHeader icon={GitBranch} title="Rule Engine" />
<CustomButton
variant="outlined"
className="!border-[#1E7D5C] !text-[#1E7D5C] hover:!bg-[#E8F3EF] !rounded-[10px]"
leftIcon={<Plus size={16} />}
onClick={handleAddRule}
>
Add Strategic Rule
</CustomButton>
</div>
<div className="flex flex-col gap-6">
{rules.map((rule, ruleIndex) => (
<div key={rule.id} className="border border-gray-100 rounded-[12px] p-6 bg-[#FAFAFA] relative">
{/* Rule Header */}
<div className="flex items-center justify-between mb-5">
<h3 className="text-base font-bold text-[#0F172B]">Rule {ruleIndex + 1}</h3>
{rules.length > 1 && (
<button
onClick={() => handleDeleteRule(rule.id)}
className="p-1.5 text-red-500 hover:bg-red-50 rounded-md transition-colors"
>
<Trash2 size={18} />
</button>
)}
</div>
{/* Rule Settings */}
<div className="flex items-end gap-6 mb-8">
<div className="w-1/3 flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Rule Category</label>
<CustomDropdown
options={RULE_CATEGORY_OPTIONS}
value={rule.category}
onChange={(val) => handleUpdateRule(rule.id, { category: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-[13px] font-semibold text-gray-700">Priority</label>
<div className="flex items-center border border-gray-200 rounded-[10px] h-11 bg-white overflow-hidden w-[120px]">
<button
onClick={() => handleUpdateRule(rule.id, { priority: Math.max(1, rule.priority - 1) })}
className="flex-1 flex items-center justify-center h-full hover:bg-gray-50 text-gray-500"
>
<Minus size={16} />
</button>
<span className="flex-1 text-center text-[14px] font-semibold text-gray-800">
{rule.priority}
</span>
<button
onClick={() => handleUpdateRule(rule.id, { priority: Math.min(10, rule.priority + 1) })}
className="flex-1 flex items-center justify-center h-full hover:bg-gray-50 text-gray-500"
>
<Plus size={16} />
</button>
</div>
{rules.some(r => r.id !== rule.id && r.priority === rule.priority) && (
<span className="text-red-500 text-xs mt-1">Priority already exists</span>
)}
</div>
</div>
{/* Visual Condition Builder */}
<div className="mb-8">
<h4 className="text-[14px] font-bold text-[#0F172B] mb-4">Visual Condition Builder</h4>
<div className="flex flex-col gap-3">
{rule.conditions.map((condition, cIdx) => (
<div key={condition.id} className="flex items-end gap-3">
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Condition</label>
<CustomDropdown
options={CONDITION_OPTIONS}
value={condition.condition}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { condition: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Operator</label>
<CustomDropdown
options={OPERATOR_OPTIONS}
value={condition.operator}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { operator: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Value</label>
<CustomInput
value={condition.value}
onChange={(e) => handleUpdateCondition(rule.id, condition.id, { value: e.target.value })}
placeholder="Selected Option"
/>
</div>
{cIdx < rule.conditions.length - 1 ? (
<div className="w-[120px] flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Logic</label>
<CustomDropdown
options={LOGIC_OPTIONS}
value={condition.logic}
onChange={(val) => handleUpdateCondition(rule.id, condition.id, { logic: val })}
placeholder="AND"
/>
</div>
) : (
<div className="flex items-center">
<CustomButton
variant="secondary"
className="!bg-[#E8F3EF] !text-[#1E7D5C] hover:!bg-[#d9ece4] !px-3 !h-11"
leftIcon={<Plus size={16} />}
onClick={() => handleAddCondition(rule.id)}
>
Add Condition
</CustomButton>
</div>
)}
<div className="flex items-center h-11">
{rule.conditions.length > 1 && (
<button
onClick={() => handleDeleteCondition(rule.id, condition.id)}
className="w-11 h-11 flex items-center justify-center bg-red-50 text-red-500 rounded-[10px] hover:bg-red-100 transition-colors"
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
))}
</div>
</div>
{/* Strategic Action Builder */}
<div>
<h4 className="text-[14px] font-bold text-[#0F172B] mb-4">Strategic Action Builder</h4>
<div className="flex flex-col gap-3">
{rule.actions.map((action, aIdx) => (
<div key={action.id} className="flex items-end gap-3">
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Action Type</label>
<CustomDropdown
options={ACTION_TYPE_OPTIONS}
value={action.actionType}
onChange={(val) => handleUpdateAction(rule.id, action.id, { actionType: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Room Type / Tier</label>
<CustomDropdown
options={ROOM_TYPE_TIER_OPTIONS}
value={action.roomTypeTier}
onChange={(val) => handleUpdateAction(rule.id, action.id, { roomTypeTier: val })}
placeholder="Selected Option"
/>
</div>
<div className="flex-1 flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Duration</label>
<CustomInput
value={action.duration}
onChange={(e) => handleUpdateAction(rule.id, action.id, { duration: e.target.value })}
placeholder="Selected Option"
/>
</div>
{aIdx < rule.actions.length - 1 ? (
<div className="w-[120px] flex flex-col gap-2">
<label className="text-[12px] font-medium text-gray-500">Logic</label>
<CustomDropdown
options={LOGIC_OPTIONS}
value={action.logic}
onChange={(val) => handleUpdateAction(rule.id, action.id, { logic: val })}
placeholder="AND"
/>
</div>
) : (
<div className="flex items-center">
<CustomButton
variant="secondary"
className="!bg-[#E8F3EF] !text-[#1E7D5C] hover:!bg-[#d9ece4] !px-3 !h-11"
leftIcon={<Plus size={16} />}
onClick={() => handleAddAction(rule.id)}
>
Add
</CustomButton>
</div>
)}
<div className="flex items-center h-11">
{rule.actions.length > 1 && (
<button
onClick={() => handleDeleteAction(rule.id, action.id)}
className="w-11 h-11 flex items-center justify-center bg-red-50 text-red-500 rounded-[10px] hover:bg-red-100 transition-colors"
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* ─── Sticky Footer ─────────────────────────────────────────────── */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-8 py-4 flex items-center justify-between z-10 shadow-[0_-4px_10px_rgba(0,0,0,0.02)]">
<div className="flex items-center gap-3">
<span className="text-[14px] font-semibold text-gray-600">Status:</span>
<CustomStatus status="Active" />
</div>
<div className="flex items-center gap-3">
<CustomButton
variant="secondary"
className="!text-[#1E7D5C] !bg-[#E8F3EF] hover:!bg-[#d9ece4] !border-none font-semibold px-6"
>
Save Draft
</CustomButton>
<CustomButton
variant="outlined"
className="!border-[#1E7D5C] !text-[#1E7D5C] hover:!bg-gray-50 font-semibold px-6"
onClick={() => navigate('/policy-engine')}
>
Cancel Policy
</CustomButton>
<CustomButton
variant="primary"
className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm"
>
Deploy Policy
</CustomButton>
</div>
</div>
</div>
);
}
@@ -0,0 +1,314 @@
import { useState, useEffect, useCallback } from 'react';
import { Plus, Trash2, Search, Check, X, Pencil, Copy } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import {
CustomTable,
CustomInput,
CustomButton,
CustomStatus,
CustomActionMenu,
CustomActionItem,
CustomConfirmationModal,
Skeleton,
} from '../../../components/custom';
import type { Column } from '../../../components/custom/CustomTable';
import type { PolicyEngineResponse } from '../PolicyEngineTypes';
// ─── Constants ───────────────────────────────────────────────────────────────
const PAGE_SIZE = 10;
// ─── Mock Data ───────────────────────────────────────────────────────────────
const MOCK_POLICIES: PolicyEngineResponse[] = [
{
id: '1',
policyName: 'New Recovery Strategy',
jurisdiction: 'GLOBAL',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
},
{
id: '2',
policyName: 'EU261 Standard Recovery',
jurisdiction: 'GLOBAL',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
},
{
id: '3',
policyName: 'US DOT Consumer Protection',
jurisdiction: 'GLOBAL',
status: 'Active',
lastModified: '4 Jun 2026, 4:09pm',
},
// Add some more mock data to demonstrate pagination if needed
...Array.from({ length: 9 }).map((_, i) => ({
id: `mock-${i + 4}`,
policyName: `Sample Policy ${i + 4}`,
jurisdiction: 'GLOBAL',
status: i % 2 === 0 ? 'Draft' : 'Inactive' as 'Active' | 'Inactive' | 'Draft',
lastModified: '5 Jun 2026, 10:00am',
}))
];
// ─── Helpers ─────────────────────────────────────────────────────────────────
function HeaderLabel({ text }: { text: string }) {
return (
<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>
);
}
function BadgeLabel({ text }: { text: string }) {
return (
<span className="px-3 py-1 bg-gray-100 text-gray-500 rounded-full text-xs font-semibold">
{text}
</span>
);
}
// ─── Component ───────────────────────────────────────────────────────────────
export default function PolicyEngineList() {
const navigate = useNavigate();
const [policies, setPolicies] = useState<PolicyEngineResponse[]>([]);
const [loading, setLoading] = useState(true);
// Pagination
const [currentPage, setCurrentPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(1);
// Filters
const [search, setSearch] = useState('');
// Modal state
const [deleteTarget, setDeleteTarget] = useState<PolicyEngineResponse | null>(null);
const [deactivateTarget, setDeactivateTarget] = useState<PolicyEngineResponse | null>(null);
// ─── Fetch data ─────────────────────────────────────────────
const fetchPolicies = useCallback((page: number) => {
setLoading(true);
// Simulate API call with timeout
setTimeout(() => {
const filteredData = MOCK_POLICIES.filter(p =>
p.policyName.toLowerCase().includes(search.toLowerCase())
);
const total = filteredData.length;
const pages = Math.ceil(total / PAGE_SIZE);
const start = (page - 1) * PAGE_SIZE;
const paginatedData = filteredData.slice(start, start + PAGE_SIZE);
setPolicies(paginatedData);
setTotalItems(total);
setTotalPages(pages || 1);
setLoading(false);
}, 500);
}, [search]);
useEffect(() => {
fetchPolicies(currentPage);
}, [currentPage, fetchPolicies]);
// ─── Handlers ──────────────────────────────────────────────────────────────
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleSearchChange = (val: string) => {
setSearch(val);
setCurrentPage(1);
};
const handleDelete = async () => {
if (!deleteTarget) return;
// Simulate delete
console.log('Deleted policy:', deleteTarget.id);
setDeleteTarget(null);
fetchPolicies(currentPage);
};
const handleToggleStatus = async () => {
if (!deactivateTarget) return;
// Simulate toggle status
console.log('Toggled status for policy:', deactivateTarget.id);
setDeactivateTarget(null);
fetchPolicies(currentPage);
};
const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
// ─── Table columns ─────────────────────────────────────────────────────────
const columns: Column<PolicyEngineResponse>[] = [
{
header: <HeaderLabel text="Policy Name" />,
accessor: row => (
<span className="text-[13px] font-semibold text-[#0F172B] leading-[18px] tracking-[0px]">
{row.policyName}
</span>
),
},
{
header: <HeaderLabel text="Jurisdiction" />,
accessor: row => <BadgeLabel text={row.jurisdiction} />,
},
{
header: <HeaderLabel text="Status" />,
accessor: row => <CustomStatus status={row.status} />,
},
{
header: <HeaderLabel text="Last Modified" />,
accessor: row => <CellText text={row.lastModified} />,
},
{
header: <HeaderLabel text="Action" />,
className: 'text-right',
accessor: row => (
<CustomActionMenu>
{row.status !== 'Active' && (
<CustomActionItem
icon={<Check size={15} />}
variant="success"
onClick={() => setDeactivateTarget(row)}
>
Activate
</CustomActionItem>
)}
{row.status === 'Active' && (
<CustomActionItem
icon={<X size={15} />}
onClick={() => setDeactivateTarget(row)}
>
Deactivate
</CustomActionItem>
)}
<CustomActionItem
icon={<Copy size={15} />}
onClick={() => console.log('Duplicate:', row.id)}
>
Duplicate
</CustomActionItem>
<CustomActionItem
icon={<Pencil size={15} />}
onClick={() => console.log('Edit:', row.id)}
>
Edit
</CustomActionItem>
<CustomActionItem
icon={<Trash2 size={15} />}
variant="danger"
onClick={() => setDeleteTarget(row)}
>
Delete
</CustomActionItem>
</CustomActionMenu>
),
},
];
// ─── Loading skeleton ──────────────────────────────────────────────────────
if (loading) {
return (
<div className="w-full flex flex-col bg-white rounded-[20px] shadow-sm border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-gray-100">
<Skeleton width={320} height={36} />
<div className="flex items-center gap-3">
<Skeleton width={148} height={36} />
</div>
</div>
<div className="p-6 flex flex-col gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} height={52} />
))}
</div>
</div>
);
}
// ─── Render ────────────────────────────────────────────────────────────────
return (
<>
<CustomTable<PolicyEngineResponse>
columns={columns}
data={policies}
leftHeaderActions={
<div className="w-[380px]">
<CustomInput
placeholder="Search framework registry..."
value={search}
onChange={e => handleSearchChange(e.target.value)}
leftIcon={<Search size={16} />}
className="!bg-[#F3F6F5] !rounded-[10px] !h-[38px] !border !border-[#E5E7EB]"
containerClassName="!gap-0"
/>
</div>
}
rightHeaderActions={
<>
<CustomButton
variant="primary"
size="md"
leftIcon={<Plus size={16} />}
className="!rounded-[10px] !gap-[10px] !h-[40px] !bg-[#1E7D5C] hover:!bg-[#17664B]"
onClick={() => navigate('/policy-engine/add')}
>
Deploy New Policy
</CustomButton>
</>
}
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
startIndex={startIndex}
endIndex={endIndex}
onPageChange={handlePageChange}
itemName="Policies"
/>
{/* Delete Confirmation */}
<CustomConfirmationModal
isOpen={!!deleteTarget}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}
title="Delete Policy"
description={`"${deleteTarget?.policyName}" will be permanently removed.`}
confirmText="Delete"
cancelText="Cancel"
variant="danger"
/>
{/* Activate / Deactivate Confirmation */}
<CustomConfirmationModal
isOpen={!!deactivateTarget}
onClose={() => setDeactivateTarget(null)}
onConfirm={handleToggleStatus}
title={deactivateTarget?.status === 'Active' ? 'Deactivate Policy' : 'Activate Policy'}
description={
deactivateTarget?.status === 'Active'
? `"${deactivateTarget?.policyName}" will be deactivated.`
: `"${deactivateTarget?.policyName}" will be reactivated.`
}
confirmText={deactivateTarget?.status === 'Active' ? 'Deactivate' : 'Activate'}
cancelText="Cancel"
variant="warning"
/>
</>
);
}
+1 -1
View File
@@ -19,7 +19,7 @@ const NAV_ITEMS = [
{ label: 'Simulation Engine', path: '/simulation', icon: Settings2 },
{ label: 'Recovery Incidents', path: '/recovery', icon: RefreshCcw, dot: true },
{ label: 'Cohort Management', path: '/cohorts', icon: Users },
{ label: 'Policy Engine', path: '/policy', icon: ShieldCheck },
{ label: 'Policy Engine', path: '/policy-engine', icon: ShieldCheck },
{ label: 'Configuration', path: '/config', icon: Settings },
{ label: 'Audit Logs', path: '/audit', icon: History },
];