Merge pull request 'Policy engine screens' (#13) from azeem into development
Reviewed-on: https://gitea.maskantech.in/gitea_admin/aeroresolve_frontend/pulls/13 Reviewed-by: Syed Waseem khadri Rafai <waseem.khadri@maskatech.com>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user