diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx index 801b1a4..11c26df 100644 --- a/src/AppRoutes.tsx +++ b/src/AppRoutes.tsx @@ -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() { } /> } /> + } /> + } /> ) diff --git a/src/app/policyEngine/PolicyEngineApi.ts b/src/app/policyEngine/PolicyEngineApi.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/policyEngine/PolicyEngineTypes.ts b/src/app/policyEngine/PolicyEngineTypes.ts new file mode 100644 index 0000000..5e4bce7 --- /dev/null +++ b/src/app/policyEngine/PolicyEngineTypes.ts @@ -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; +} diff --git a/src/app/policyEngine/components/AddPolicyEngine.tsx b/src/app/policyEngine/components/AddPolicyEngine.tsx new file mode 100644 index 0000000..3e219ec --- /dev/null +++ b/src/app/policyEngine/components/AddPolicyEngine.tsx @@ -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([ + { + 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) => { + 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) => { + 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) => { + 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 }) => ( +
+
+ +
+

{title}

+
+ ); + + return ( +
+ + {/* ─── Header ────────────────────────────────────────────────────── */} +
+
+ +
+

Deploy New Policy

+ Global Framework Registry +
+
+
+ + } + > + Deploy Policy + +
+
+ + {/* ─── Body Content ──────────────────────────────────────────────── */} +
+
+ + {/* Policy Information Card */} +
+ + +
+
+ + setPolicyName(e.target.value)} + placeholder="Selected Option" + className="!h-11" + /> +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + setDescription(e.target.value)} + placeholder="Enter description..." + className="!h-24 resize-none" + /> +
+
+ + {/* Target Audience Card */} +
+ + +
+
+ + +
+ + {audienceType === 'Selected Cohorts' && ( +
+ + +
+ )} +
+
+ + {/* Rule Engine Card */} +
+
+ + } + onClick={handleAddRule} + > + Add Strategic Rule + +
+ +
+ {rules.map((rule, ruleIndex) => ( +
+ + {/* Rule Header */} +
+

Rule {ruleIndex + 1}

+ {rules.length > 1 && ( + + )} +
+ + {/* Rule Settings */} +
+
+ + handleUpdateRule(rule.id, { category: val })} + placeholder="Selected Option" + /> +
+
+ +
+ + + {rule.priority} + + +
+ {rules.some(r => r.id !== rule.id && r.priority === rule.priority) && ( + Priority already exists + )} +
+
+ + {/* Visual Condition Builder */} +
+

Visual Condition Builder

+
+ {rule.conditions.map((condition, cIdx) => ( +
+
+ + handleUpdateCondition(rule.id, condition.id, { condition: val })} + placeholder="Selected Option" + /> +
+
+ + handleUpdateCondition(rule.id, condition.id, { operator: val })} + placeholder="Selected Option" + /> +
+
+ + handleUpdateCondition(rule.id, condition.id, { value: e.target.value })} + placeholder="Selected Option" + /> +
+ {cIdx < rule.conditions.length - 1 ? ( +
+ + handleUpdateCondition(rule.id, condition.id, { logic: val })} + placeholder="AND" + /> +
+ ) : ( +
+ } + onClick={() => handleAddCondition(rule.id)} + > + Add Condition + +
+ )} +
+ {rule.conditions.length > 1 && ( + + )} +
+
+ ))} +
+
+ + {/* Strategic Action Builder */} +
+

Strategic Action Builder

+
+ {rule.actions.map((action, aIdx) => ( +
+
+ + handleUpdateAction(rule.id, action.id, { actionType: val })} + placeholder="Selected Option" + /> +
+
+ + handleUpdateAction(rule.id, action.id, { roomTypeTier: val })} + placeholder="Selected Option" + /> +
+
+ + handleUpdateAction(rule.id, action.id, { duration: e.target.value })} + placeholder="Selected Option" + /> +
+ {aIdx < rule.actions.length - 1 ? ( +
+ + handleUpdateAction(rule.id, action.id, { logic: val })} + placeholder="AND" + /> +
+ ) : ( +
+ } + onClick={() => handleAddAction(rule.id)} + > + Add + +
+ )} +
+ {rule.actions.length > 1 && ( + + )} +
+
+ ))} +
+
+ +
+ ))} +
+ +
+ +
+
+ + {/* ─── Sticky Footer ─────────────────────────────────────────────── */} +
+
+ Status: + +
+
+ + Save Draft + + navigate('/policy-engine')} + > + Cancel Policy + + + Deploy Policy + +
+
+ +
+ ); +} diff --git a/src/app/policyEngine/components/PolicyEngineList.tsx b/src/app/policyEngine/components/PolicyEngineList.tsx new file mode 100644 index 0000000..ff9ab66 --- /dev/null +++ b/src/app/policyEngine/components/PolicyEngineList.tsx @@ -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 ( + {text} + ); +} + +function CellText({ text }: { text: string | null | undefined }) { + return ( + + {text || '—'} + + ); +} + +function BadgeLabel({ text }: { text: string }) { + return ( + + {text} + + ); +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +export default function PolicyEngineList() { + const navigate = useNavigate(); + const [policies, setPolicies] = useState([]); + 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(null); + const [deactivateTarget, setDeactivateTarget] = useState(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[] = [ + { + header: , + accessor: row => ( + + {row.policyName} + + ), + }, + { + header: , + accessor: row => , + }, + { + header: , + accessor: row => , + }, + { + header: , + accessor: row => , + }, + { + header: , + className: 'text-right', + accessor: row => ( + + {row.status !== 'Active' && ( + } + variant="success" + onClick={() => setDeactivateTarget(row)} + > + Activate + + )} + {row.status === 'Active' && ( + } + onClick={() => setDeactivateTarget(row)} + > + Deactivate + + )} + } + onClick={() => console.log('Duplicate:', row.id)} + > + Duplicate + + } + onClick={() => console.log('Edit:', row.id)} + > + Edit + + } + variant="danger" + onClick={() => setDeleteTarget(row)} + > + Delete + + + ), + }, + ]; + + // ─── Loading skeleton ────────────────────────────────────────────────────── + + if (loading) { + return ( +
+
+ +
+ +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+
+ ); + } + + // ─── Render ──────────────────────────────────────────────────────────────── + + return ( + <> + + columns={columns} + data={policies} + leftHeaderActions={ +
+ handleSearchChange(e.target.value)} + leftIcon={} + className="!bg-[#F3F6F5] !rounded-[10px] !h-[38px] !border !border-[#E5E7EB]" + containerClassName="!gap-0" + /> +
+ } + rightHeaderActions={ + <> + } + className="!rounded-[10px] !gap-[10px] !h-[40px] !bg-[#1E7D5C] hover:!bg-[#17664B]" + onClick={() => navigate('/policy-engine/add')} + > + Deploy New Policy + + + } + currentPage={currentPage} + totalPages={totalPages} + totalItems={totalItems} + startIndex={startIndex} + endIndex={endIndex} + onPageChange={handlePageChange} + itemName="Policies" + /> + + {/* Delete Confirmation */} + setDeleteTarget(null)} + onConfirm={handleDelete} + title="Delete Policy" + description={`"${deleteTarget?.policyName}" will be permanently removed.`} + confirmText="Delete" + cancelText="Cancel" + variant="danger" + /> + + {/* Activate / Deactivate Confirmation */} + 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" + /> + + ); +} diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx index 3258ead..3eb87ab 100644 --- a/src/layout/AppSidebar.tsx +++ b/src/layout/AppSidebar.tsx @@ -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 }, ];