From 3e01d757c74d2813d47be594543e762010937fec Mon Sep 17 00:00:00 2001
From: azeeee05
Date: Tue, 8 Sep 2026 14:44:16 +0530
Subject: [PATCH] feat: initialize core frontend modules including user
management, role-based access, and configuration interfaces
---
src/App.tsx | 13 +-
src/AppRoutes.tsx | 177 ++++++--
src/app/api/ApiClient.ts | 80 +++-
.../auditLogs/components/AuditLogsList.tsx | 19 +-
src/app/authentication/index.tsx | 262 ++++++++++-
.../cohartManage/components/cohartList.tsx | 81 ++--
.../components/ActionTypesColumn.tsx | 61 +--
.../components/CategoriesColumn.tsx | 59 +--
.../components/ConfigurationFieldsColumn.tsx | 60 +--
.../masterData/components/MasterItemTable.tsx | 53 ++-
src/app/dashboard/index.tsx | 18 +-
.../components/AddPolicyEngine.tsx | 37 +-
.../components/PolicyEngineList.tsx | 81 ++--
.../components/RecoveryIncidentsList.tsx | 99 +++--
src/app/roles/RolesApi.ts | 69 +++
src/app/roles/components/AddRoles.tsx | 355 +++++++++++++++
src/app/roles/components/RolesList.tsx | 263 +++++++++++
src/app/roles/index.tsx | 44 ++
.../components/SimulationTerminal.tsx | 47 +-
src/app/users/UsersApi.ts | 67 +++
.../users/components/ResetPasswordModal.tsx | 110 +++++
src/app/users/components/UserModal.tsx | 264 ++++++++++++
src/app/users/components/UsersList.tsx | 407 ++++++++++++++++++
src/app/users/index.tsx | 9 +
src/components/common/Can.tsx | 40 ++
src/components/common/ProtectedRoute.tsx | 70 +++
src/context/AuthContext.tsx | 142 ++++++
src/layout/AppHeader.tsx | 3 +
src/layout/AppSidebar.tsx | 57 ++-
29 files changed, 2707 insertions(+), 340 deletions(-)
create mode 100644 src/app/roles/RolesApi.ts
create mode 100644 src/app/roles/components/AddRoles.tsx
create mode 100644 src/app/roles/components/RolesList.tsx
create mode 100644 src/app/roles/index.tsx
create mode 100644 src/app/users/UsersApi.ts
create mode 100644 src/app/users/components/ResetPasswordModal.tsx
create mode 100644 src/app/users/components/UserModal.tsx
create mode 100644 src/app/users/components/UsersList.tsx
create mode 100644 src/app/users/index.tsx
create mode 100644 src/components/common/Can.tsx
create mode 100644 src/components/common/ProtectedRoute.tsx
create mode 100644 src/context/AuthContext.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 05715ff..804d6d7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,12 +1,15 @@
-import { BrowserRouter } from 'react-router-dom'
-import AppRoutes from './AppRoutes'
+import { BrowserRouter } from 'react-router-dom';
+import { AuthProvider } from './context/AuthContext';
+import AppRoutes from './AppRoutes';
function App() {
return (
-
+
+
+
- )
+ );
}
-export default App
+export default App;
diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx
index 32da98b..66e08a0 100644
--- a/src/AppRoutes.tsx
+++ b/src/AppRoutes.tsx
@@ -1,40 +1,157 @@
import { lazy, Suspense } from "react";
import { Route, Routes, Navigate } from "react-router-dom";
import Layout from "./layout/AppLayout";
+import ProtectedRoute from "./components/common/ProtectedRoute";
-
-const HomePage = lazy(() => import('./app/dashboard'))
-const CohortManage = lazy(() => import('./app/cohartManage'))
-const PolicyEngineList = lazy(() => import('./app/policyEngine/components/PolicyEngineList'))
-const AddPolicyEngine = lazy(() => import('./app/policyEngine/components/AddPolicyEngine'))
-const RecoveryIncidentsList = lazy(() => import('./app/recoveryIncidents/components/RecoveryIncidentsList'))
-const RecoveryIncidentTabs = lazy(() => import('./app/recoveryIncidents/tabs/index'))
-const AuditLogsList = lazy(() => import('./app/auditLogs/components/AuditLogsList'))
-const ConfigurationPage = lazy(() => import('./app/configuration'))
-const SimulationPage = lazy(() => import('./app/simulation'))
+const LoginPage = lazy(() => import("./app/authentication"));
+const HomePage = lazy(() => import("./app/dashboard"));
+const SimulationPage = lazy(() => import("./app/simulation"));
+const CohortManage = lazy(() => import("./app/cohartManage"));
+const PolicyEngineList = lazy(() => import("./app/policyEngine/components/PolicyEngineList"));
+const AddPolicyEngine = lazy(() => import("./app/policyEngine/components/AddPolicyEngine"));
+const RecoveryIncidentsList = lazy(() => import("./app/recoveryIncidents/components/RecoveryIncidentsList"));
+const RecoveryIncidentTabs = lazy(() => import("./app/recoveryIncidents/tabs/index"));
+const ConfigurationPage = lazy(() => import("./app/configuration"));
+const AuditLogsList = lazy(() => import("./app/auditLogs/components/AuditLogsList"));
+const UsersPage = lazy(() => import("./app/users"));
+const RolesPage = lazy(() => import("./app/roles"));
function AppRoutes() {
return (
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- } />
- } />
- } />
- } />
-
-
-
+
+
+
+ }
+ >
+
+ {/* Public Login Route */}
+ } />
+
+ {/* Protected Application Routes */}
+
+
+
+
+
+ }
+ >
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+
+
+
+ }
+ />
+
+
);
}
diff --git a/src/app/api/ApiClient.ts b/src/app/api/ApiClient.ts
index 959d4ed..1ea208f 100644
--- a/src/app/api/ApiClient.ts
+++ b/src/app/api/ApiClient.ts
@@ -1,24 +1,38 @@
-import axios from 'axios';
+import axios, { AxiosError, type InternalAxiosRequestConfig } from 'axios';
-const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001';
+const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
-// Create a configured Axios instance
+// Create a configured Axios instance with credentials enabled for secure HTTP-only cookies
export const ApiClient = axios.create({
baseURL: API_BASE,
+ withCredentials: true,
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': 'demo-airline',
},
});
+let isRefreshing = false;
+let failedQueue: Array<{
+ resolve: (value?: any) => void;
+ reject: (reason?: any) => void;
+}> = [];
+
+const processQueue = (error: any, token: any = null) => {
+ failedQueue.forEach((prom) => {
+ if (error) {
+ prom.reject(error);
+ } else {
+ prom.resolve(token);
+ }
+ });
+ failedQueue = [];
+};
+
// Request Interceptor
ApiClient.interceptors.request.use(
- (config) => {
- // You can attach authorization tokens here if needed
- // const token = localStorage.getItem('token');
- // if (token) {
- // config.headers.Authorization = `Bearer ${token}`;
- // }
+ (config: InternalAxiosRequestConfig) => {
+ // Cookies are automatically sent via withCredentials: true
return config;
},
(error) => {
@@ -26,17 +40,53 @@ ApiClient.interceptors.request.use(
}
);
-// Response Interceptor
+// Response Interceptor with automatic silent refresh
ApiClient.interceptors.response.use(
(response) => {
- // Return just the data payload by default for convenience
return response.data;
},
- (error) => {
- // Handle global API errors here (e.g., redirect on 401 Unauthorized)
- if (error.response?.status === 401) {
- console.error('Unauthorized access - perhaps redirect to login?');
+ async (error: AxiosError) => {
+ const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
+
+ const isAuthUrl =
+ originalRequest?.url?.includes('/auth/login') ||
+ originalRequest?.url?.includes('/auth/refresh') ||
+ originalRequest?.url?.includes('/auth/me');
+
+ if (error.response?.status === 401 && !originalRequest?._retry && !isAuthUrl) {
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({ resolve, reject });
+ })
+ .then(() => ApiClient(originalRequest))
+ .catch((err) => Promise.reject(err));
+ }
+
+ originalRequest._retry = true;
+ isRefreshing = true;
+
+ try {
+ await axios.post(
+ `${API_BASE}/auth/refresh`,
+ {},
+ { withCredentials: true }
+ );
+ processQueue(null);
+ return ApiClient(originalRequest);
+ } catch (refreshError) {
+ processQueue(refreshError, null);
+ // Only redirect if not already on the login page
+ if (!window.location.pathname.startsWith('/login')) {
+ window.location.href = '/login';
+ }
+ return Promise.reject(refreshError);
+ } finally {
+ isRefreshing = false;
+ }
}
+
return Promise.reject(error);
}
);
+
+export default ApiClient;
diff --git a/src/app/auditLogs/components/AuditLogsList.tsx b/src/app/auditLogs/components/AuditLogsList.tsx
index 47a533b..2fab338 100644
--- a/src/app/auditLogs/components/AuditLogsList.tsx
+++ b/src/app/auditLogs/components/AuditLogsList.tsx
@@ -18,6 +18,7 @@ import type { AuditLog, AuditLogFilters } from "../AuditLogsTypes";
import { AUDIT_MODULE_LABELS, AUDIT_ACTION_VARIANTS } from "../AuditLogsTypes";
import { getAuditLogs } from "../AuditLogsApi";
import AuditLogDetail from "./AuditLogDetail";
+import Can from "../../../components/common/Can";
const PAGE_SIZE = 20;
@@ -335,14 +336,16 @@ export default function AuditLogsList() {
className="!border-[#1E8E3E] !bg-[#E6F4EA] !h-[40px] hover:!bg-[#E6F4EA]"
/>
-
-
- Export CSV
-
+
+
+
+ Export CSV
+
+
}
currentPage={currentPage}
diff --git a/src/app/authentication/index.tsx b/src/app/authentication/index.tsx
index 0be90de..3c0a7ef 100644
--- a/src/app/authentication/index.tsx
+++ b/src/app/authentication/index.tsx
@@ -1,10 +1,254 @@
-function AboutPage() {
- return (
-
- About Page
- This is the about page. Add your about content here.
-
- )
-}
+import React, { useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import {
+ Lock,
+ Mail,
+ Eye,
+ EyeOff,
+ ShieldCheck,
+ Plane,
+ KeyRound,
+ Sparkles,
+ ArrowRight,
+ AlertCircle,
+} from 'lucide-react';
-export default AboutPage
+export default function LoginPage() {
+ const { login, isAuthenticated } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // If already authenticated, redirect
+ React.useEffect(() => {
+ if (isAuthenticated) {
+ const from = (location.state as any)?.from?.pathname || '/';
+ navigate(from, { replace: true });
+ }
+ }, [isAuthenticated, navigate, location]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!email || !password) {
+ setError('Please provide both email and password.');
+ return;
+ }
+
+ setError(null);
+ setLoading(true);
+
+ try {
+ await login(email, password);
+ const from = (location.state as any)?.from?.pathname || '/';
+ navigate(from, { replace: true });
+ } catch (err: any) {
+ const msg =
+ err?.response?.data?.message ||
+ err?.message ||
+ 'Failed to authenticate. Please check your credentials.';
+ setError(msg);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleQuickFill = () => {
+ setEmail('admin@aeroresolve.com');
+ setPassword('Password@123');
+ setError(null);
+ };
+
+ return (
+
+ {/* Left / Hero Column */}
+
+ {/* Background glow & radar circles */}
+
+
+
+
+ {/* Top Logo */}
+
+
+
+
+ AERO RESOLVE
+
+ Enterprise
+
+
+
+ Aviation Passenger Recovery & Compensation Intelligence
+
+
+
+
+ {/* Hero Central Content */}
+
+
+
+
+ Secure Role-Based Access Control (RBAC)
+
+
+
+
+ Next-Generation Flight Disruption Orchestration
+
+
+
+ Automate passenger segmentation, multi-jurisdiction regulatory compensation, goodwill
+ policies, and live manifest recovery simulations in real time.
+
+
+
+
+ 99.98%
+ Uptime Reliability
+
+
+ < 150ms
+ Policy Engine Evaluation
+
+
+
+
+ {/* Bottom Trust Footer */}
+
+ Aero Resolve Operating System v1.0
+ Aerospace High-Security Protocol
+
+
+
+ {/* Right / Login Form Column */}
+
+
+ {/* Header Mobile / Title */}
+
+
+
+
+ Sign In to Terminal
+
+
+ Enter your credentials to access operations management.
+
+
+
+ {/* Quick Demo Fill Helper */}
+
+
+
+
+ Super Admin Demo Credentials
+ admin@aeroresolve.com
+
+
+
+ Fill Credentials
+
+
+
+ {/* Error Banner */}
+ {error && (
+
+ )}
+
+ {/* Form */}
+
+
+ {/* Security badge footer */}
+
+
+ Encrypted with HTTP-Only Cookie Authentication
+
+
+
+
+ );
+}
diff --git a/src/app/cohartManage/components/cohartList.tsx b/src/app/cohartManage/components/cohartList.tsx
index 4c14c91..d6c5075 100644
--- a/src/app/cohartManage/components/cohartList.tsx
+++ b/src/app/cohartManage/components/cohartList.tsx
@@ -27,6 +27,7 @@ import {
updateCohartStatus,
} from "../CohartManageApi";
import type { CohartResponse } from "../CohartManageTypes";
+import Can from "../../../components/common/Can";
// ─── Constants ───────────────────────────────────────────────────────────────
@@ -191,35 +192,43 @@ export default function CohortList() {
accessor: (row) => (
{row.status !== "Active" && (
- }
- variant="success"
- onClick={() => setDeactivateTarget(row)}
- >
- Activate
-
+
+ }
+ variant="success"
+ onClick={() => setDeactivateTarget(row)}
+ >
+ Activate
+
+
)}
{row.status === "Active" && (
- }
- onClick={() => setDeactivateTarget(row)}
- >
- Deactivate
-
+
+ }
+ onClick={() => setDeactivateTarget(row)}
+ >
+ Deactivate
+
+
)}
- }
- onClick={() => setEditTarget(row)}
- >
- Edit
-
- }
- variant="danger"
- onClick={() => setDeleteTarget(row)}
- >
- Delete
-
+
+ }
+ onClick={() => setEditTarget(row)}
+ >
+ Edit
+
+
+
+ }
+ variant="danger"
+ onClick={() => setDeleteTarget(row)}
+ >
+ Delete
+
+
),
},
@@ -291,15 +300,17 @@ export default function CohortList() {
size="md"
/>
- }
- className="!rounded-[10px] !gap-[10px] !h-[40px]"
- onClick={() => setIsAddOpen(true)}
- >
- Create Cohort
-
+
+ }
+ className="!rounded-[10px] !gap-[10px] !h-[40px]"
+ onClick={() => setIsAddOpen(true)}
+ >
+ Create Cohort
+
+
>
}
currentPage={currentPage}
diff --git a/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx b/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx
index 9eb4e07..d6850b6 100644
--- a/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx
+++ b/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { PlusIcon, MagnifyingGlassIcon, PencilSimpleIcon, TrashIcon } from '@phosphor-icons/react';
import { CustomInput, CustomButton } from '../../../../components/custom';
import type { ActionType } from '../ActionBuilderTypes';
+import Can from '../../../../components/common/Can';
interface ActionTypesColumnProps {
actionTypes: ActionType[];
@@ -40,15 +41,17 @@ export function ActionTypesColumn({
{/* Column Header */}
Action Types
- }
- >
- Add
-
+
+ }
+ >
+ Add
+
+
@@ -114,24 +117,28 @@ export function ActionTypesColumn({
{/* Hover Actions */}
-
onOpenEdit(t, e)}
- title="Edit Action Type"
- className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-[#1E7D5C] hover:bg-white'
- }`}
- >
-
-
-
onDelete(t, e)}
- title="Delete Action Type"
- className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-red-600 hover:bg-white'
- }`}
- >
-
-
+
+ onOpenEdit(t, e)}
+ title="Edit Action Type"
+ className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-[#1E7D5C] hover:bg-white'
+ }`}
+ >
+
+
+
+
+ onDelete(t, e)}
+ title="Delete Action Type"
+ className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-red-600 hover:bg-white'
+ }`}
+ >
+
+
+
{/* Count Badge */}
diff --git a/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx b/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx
index 3c39b64..84ac5a5 100644
--- a/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx
+++ b/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { PlusIcon, MagnifyingGlassIcon, PencilSimpleIcon, TrashIcon } from '@phosphor-icons/react';
import { CustomInput, CustomButton } from '../../../../components/custom';
import type { ActionCategory, ActionType } from '../ActionBuilderTypes';
+import Can from '../../../../components/common/Can';
interface CategoriesColumnProps {
categories: ActionCategory[];
@@ -36,14 +37,16 @@ export function CategoriesColumn({
{/* Column Header */}
Categories
- }
- >
- Add
-
+
+ }
+ >
+ Add
+
+
@@ -104,24 +107,28 @@ export function CategoriesColumn({
{/* Hover Actions */}
-
onOpenEdit(cat, e)}
- title="Edit Category"
- className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-[#1E7D5C] hover:bg-white'
- }`}
- >
-
-
-
onDelete(cat, e)}
- title="Delete Category"
- className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-red-600 hover:bg-white'
- }`}
- >
-
-
+
+ onOpenEdit(cat, e)}
+ title="Edit Category"
+ className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-[#1E7D5C] hover:bg-white'
+ }`}
+ >
+
+
+
+
+ onDelete(cat, e)}
+ title="Delete Category"
+ className={`p-1.5 rounded-lg transition-colors cursor-pointer ${isSelected ? 'text-white hover:bg-white/20' : 'text-slate-400 hover:text-red-600 hover:bg-white'
+ }`}
+ >
+
+
+
{/* Count Badge */}
diff --git a/src/app/configuration/actionBuilder/components/ConfigurationFieldsColumn.tsx b/src/app/configuration/actionBuilder/components/ConfigurationFieldsColumn.tsx
index 4b2bb2c..aaad842 100644
--- a/src/app/configuration/actionBuilder/components/ConfigurationFieldsColumn.tsx
+++ b/src/app/configuration/actionBuilder/components/ConfigurationFieldsColumn.tsx
@@ -7,8 +7,9 @@ import {
TrashIcon,
DotsSixVerticalIcon,
} from '@phosphor-icons/react';
-import { CustomInput, CustomButton, Skeleton } from '../../../../components/custom';
import type { FieldDefinition } from '../ActionBuilderTypes';
+import Can from '../../../../components/common/Can';
+import { CustomButton, CustomInput, Skeleton } from '../../../../components/custom';
interface ConfigurationFieldsColumnProps {
fields: FieldDefinition[];
@@ -47,15 +48,17 @@ export function ConfigurationFieldsColumn({
{/* Column Header */}
Configuration Field
- }
- >
- Add
-
+
+ }
+ >
+ Add
+
+
{/* Search Input */}
@@ -184,22 +187,27 @@ export function ConfigurationFieldsColumn({
|
{/* Action Buttons */}
-
onOpenEdit(f)}
- title="Edit Field"
- className="p-1 text-slate-800 hover:text-[#1E7D5C] transition-colors cursor-pointer"
- >
-
-
-
onDelete(f)}
- title="Delete Field"
- className="p-1 text-[#D9383A] hover:text-red-700 transition-colors cursor-pointer"
- >
-
-
+
+ onOpenEdit(f)}
+ title="Edit Field"
+ className="p-1 text-slate-800 hover:text-[#1E7D5C] transition-colors cursor-pointer"
+ >
+
+
+
+
+
+ onDelete(f)}
+ title="Delete Field"
+ className="p-1 text-[#D9383A] hover:text-red-700 transition-colors cursor-pointer"
+ >
+
+
+
diff --git a/src/app/configuration/masterData/components/MasterItemTable.tsx b/src/app/configuration/masterData/components/MasterItemTable.tsx
index ac7ab20..6a5e76e 100644
--- a/src/app/configuration/masterData/components/MasterItemTable.tsx
+++ b/src/app/configuration/masterData/components/MasterItemTable.tsx
@@ -14,6 +14,7 @@ import {
CustomStatus,
} from '../../../../components/custom';
import type { MasterDataCategoryItem, MasterDataItem } from '../MasterDataTypes';
+import Can from '../../../../components/common/Can';
interface MasterItemTableProps {
selectedCategory: MasterDataCategoryItem | null;
@@ -63,15 +64,17 @@ export const MasterItemTable: React.FC = ({
- }
- className="!bg-[#1E7D5C] hover:!bg-[#17664B] !rounded-[12px] !gap-[8px] !h-[40px] font-semibold text-[14px] shrink-0"
- >
- Add Master Value
-
+
+ }
+ className="!bg-[#1E7D5C] hover:!bg-[#17664B] !rounded-[12px] !gap-[8px] !h-[40px] font-semibold text-[14px] shrink-0"
+ >
+ Add Master Value
+
+
{/* Search Bar */}
@@ -155,21 +158,25 @@ export const MasterItemTable: React.FC = ({
-
onOpenEdit(item)}
- title="Edit Item"
- className="p-1.5 rounded-lg text-slate-500 hover:text-[#1E7D5C] hover:bg-[#E8F3EF] transition-colors"
- >
-
-
+
+ onOpenEdit(item)}
+ title="Edit Item"
+ className="p-1.5 rounded-lg text-slate-500 hover:text-[#1E7D5C] hover:bg-[#E8F3EF] transition-colors cursor-pointer"
+ >
+
+
+
-
onOpenDelete(item)}
- title="Delete Item"
- className="p-1.5 rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 transition-colors"
- >
-
-
+
+ onOpenDelete(item)}
+ title="Delete Item"
+ className="p-1.5 rounded-lg text-slate-400 hover:text-red-600 hover:bg-red-50 transition-colors cursor-pointer"
+ >
+
+
+
diff --git a/src/app/dashboard/index.tsx b/src/app/dashboard/index.tsx
index 5a83efa..2dd9bd6 100644
--- a/src/app/dashboard/index.tsx
+++ b/src/app/dashboard/index.tsx
@@ -30,12 +30,13 @@ export default function HomePage() {
async function loadDashboardData() {
try {
setLoading(true);
- const [metricsRes, exposureRes, mixRes, incidentsRes] = await Promise.all([
- getDashboardMetrics(),
- getExposureData(),
- getDisruptionMixData(),
- getRecentIncidents(),
- ]);
+ const [metricsRes, exposureRes, mixRes, incidentsRes] =
+ await Promise.all([
+ getDashboardMetrics(),
+ getExposureData(),
+ getDisruptionMixData(),
+ getRecentIncidents(),
+ ]);
setMetrics(metricsRes);
setExposureData(exposureRes);
@@ -57,7 +58,10 @@ export default function HomePage() {
{/* Top Stat Cards Skeleton */}
{Array.from({ length: 5 }).map((_, i) => (
-
+
diff --git a/src/app/policyEngine/components/AddPolicyEngine.tsx b/src/app/policyEngine/components/AddPolicyEngine.tsx
index 2d92e59..7a9918a 100644
--- a/src/app/policyEngine/components/AddPolicyEngine.tsx
+++ b/src/app/policyEngine/components/AddPolicyEngine.tsx
@@ -18,6 +18,7 @@ import {
Skeleton,
} from '../../../components/custom';
import CustomSuccessModal from '../../../components/custom/CustomSuccessModal';
+import Can from '../../../components/common/Can';
import {
getJurisdictionOptions,
getCohortOptions,
@@ -1648,14 +1649,16 @@ export default function AddPolicyEngine() {
- handleSavePolicy(false)}
- disabled={isSaving || !isDraftValid}
- >
- {isSaving ? 'Saving...' : 'Save Draft'}
-
+
+ handleSavePolicy(false)}
+ disabled={isSaving || !isDraftValid}
+ >
+ {isSaving ? 'Saving...' : 'Save Draft'}
+
+
Cancel Policy
- handleSavePolicy(true)}
- disabled={isSaving || !isFormValid}
- >
- {isSaving ? 'Deploying...' : isEditMode ? 'Update & Deploy Policy' : 'Deploy Policy'}
-
+
+ handleSavePolicy(true)}
+ disabled={isSaving || !isFormValid}
+ >
+ {isSaving ? 'Deploying...' : isEditMode ? 'Update & Deploy Policy' : 'Deploy Policy'}
+
+
diff --git a/src/app/policyEngine/components/PolicyEngineList.tsx b/src/app/policyEngine/components/PolicyEngineList.tsx
index 30f2d24..3476123 100644
--- a/src/app/policyEngine/components/PolicyEngineList.tsx
+++ b/src/app/policyEngine/components/PolicyEngineList.tsx
@@ -21,6 +21,7 @@ import {
} from "../../../components/custom";
import type { Column } from "../../../components/custom/CustomTable";
import type { PolicyEngineResponse } from "../PolicyEngineTypes";
+import Can from "../../../components/common/Can";
import {
getPolicies,
deletePolicy,
@@ -201,35 +202,43 @@ export default function PolicyEngineList() {
accessor: (row) => (
{row.status?.toLowerCase() === "inactive" && (
- }
- variant="success"
- onClick={() => setDeactivateTarget(row)}
- >
- Activate
-
+
+ }
+ variant="success"
+ onClick={() => setDeactivateTarget(row)}
+ >
+ Activate
+
+
)}
{row.status?.toLowerCase() === "active" && (
- }
- onClick={() => setDeactivateTarget(row)}
- >
- Deactivate
-
+
+ }
+ onClick={() => setDeactivateTarget(row)}
+ >
+ Deactivate
+
+
)}
- }
- onClick={() => navigate(`/policy-engine/add?id=${row.id}`)}
- >
- Edit
-
- }
- variant="danger"
- onClick={() => setDeleteTarget(row)}
- >
- Delete
-
+
+ }
+ onClick={() => navigate(`/policy-engine/add?id=${row.id}`)}
+ >
+ Edit
+
+
+
+ }
+ variant="danger"
+ onClick={() => setDeleteTarget(row)}
+ >
+ Delete
+
+
),
},
@@ -290,15 +299,17 @@ export default function PolicyEngineList() {
}
rightHeaderActions={
<>
-
}
- className="!rounded-[10px] !gap-[10px] !h-[40px] !bg-[#1E7D5C] hover:!bg-[#17664B]"
- onClick={() => navigate("/policy-engine/add")}
- >
- Deploy New Policy
-
+
+ }
+ className="!rounded-[10px] !gap-[10px] !h-[40px] !bg-[#1E7D5C] hover:!bg-[#17664B]"
+ onClick={() => navigate("/policy-engine/add")}
+ >
+ Deploy New Policy
+
+
>
}
currentPage={currentPage}
diff --git a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
index e7f70b7..59da888 100644
--- a/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
+++ b/src/app/recoveryIncidents/components/RecoveryIncidentsList.tsx
@@ -25,6 +25,7 @@ import {
CustomAlertBanner,
CustomSuccessModal,
} from "../../../components/custom";
+import Can from "../../../components/common/Can";
import type { Column } from "../../../components/custom/CustomTable";
import type {
RecoveryIncident,
@@ -479,38 +480,46 @@ export default function RecoveryIncidentsList() {
>
View Details
-
{
- setEditingIncident(row);
- setIsModalOpen(true);
- }}
- icon={
-
- }
- >
- Edit Incident
-
-
- }
- onClick={() => handleStatusChange(row, "Approved")}
- >
- Approve
-
-
}
- onClick={() => handleStatusChange(row, "Rejected")}
- >
- Reject
-
-
}
- onClick={() => handleStatusChange(row, "Under Review")}
- >
- Mark for Review
-
+
+ {
+ setEditingIncident(row);
+ setIsModalOpen(true);
+ }}
+ icon={
+
+ }
+ >
+ Edit Incident
+
+
+
+
+ }
+ onClick={() => handleStatusChange(row, "Approved")}
+ >
+ Approve
+
+
+
+ }
+ onClick={() => handleStatusChange(row, "Rejected")}
+ >
+ Reject
+
+
+
+ }
+ onClick={() => handleStatusChange(row, "Under Review")}
+ >
+ Mark for Review
+
+
)}
@@ -618,18 +627,20 @@ export default function RecoveryIncidentsList() {
>
{isGrouped ? "Ungroup" : "Group by Flight"}
-
}
- className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
- onClick={() => {
- setEditingIncident(null);
- setIsModalOpen(true);
- }}
- >
- New Incident
-
+
+ }
+ className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
+ onClick={() => {
+ setEditingIncident(null);
+ setIsModalOpen(true);
+ }}
+ >
+ New Incident
+
+
>
}
currentPage={currentPage}
diff --git a/src/app/roles/RolesApi.ts b/src/app/roles/RolesApi.ts
new file mode 100644
index 0000000..95f1324
--- /dev/null
+++ b/src/app/roles/RolesApi.ts
@@ -0,0 +1,69 @@
+import { ApiClient } from '../api/ApiClient';
+
+export interface PermissionItem {
+ id: string;
+ module: string;
+ action: string;
+ code: string;
+ name: string;
+ description?: string;
+ groupName: string;
+}
+
+export interface RoleItem {
+ id: string;
+ name: string;
+ slug: string;
+ description?: string;
+ isSystem: boolean;
+ permissionCount: number;
+ userCount: number;
+ permissions?: PermissionItem[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface PermissionsResponse {
+ all: PermissionItem[];
+ grouped: Record
;
+}
+
+export interface CreateRolePayload {
+ name: string;
+ description?: string;
+ permissionIds: string[];
+}
+
+export interface UpdateRolePayload {
+ name?: string;
+ description?: string;
+ permissionIds?: string[];
+}
+
+export const RolesApi = {
+ getRoles: async (): Promise => {
+ return ApiClient.get('/roles');
+ },
+
+ getRoleById: async (id: string): Promise => {
+ return ApiClient.get(`/roles/${id}`);
+ },
+
+ getPermissions: async (): Promise => {
+ return ApiClient.get('/permissions');
+ },
+
+ createRole: async (payload: CreateRolePayload): Promise => {
+ return ApiClient.post('/roles', payload);
+ },
+
+ updateRole: async (id: string, payload: UpdateRolePayload): Promise => {
+ return ApiClient.patch(`/roles/${id}`, payload);
+ },
+
+ deleteRole: async (id: string): Promise<{ success: boolean; message: string }> => {
+ return ApiClient.delete(`/roles/${id}`);
+ },
+};
+
+export default RolesApi;
diff --git a/src/app/roles/components/AddRoles.tsx b/src/app/roles/components/AddRoles.tsx
new file mode 100644
index 0000000..3ce8ea6
--- /dev/null
+++ b/src/app/roles/components/AddRoles.tsx
@@ -0,0 +1,355 @@
+import React, { useEffect, useState } from 'react';
+import { RolesApi, type PermissionItem, type RoleItem } from '../RolesApi';
+import {
+ CustomInput,
+ CustomButton,
+ CustomCheckBox,
+ CustomAlertBanner,
+ Skeleton,
+} from '../../../components/custom';
+import {
+ ShieldCheckIcon,
+ LockKeyIcon,
+ ArrowLeftIcon,
+ FloppyDiskIcon,
+ SquaresFourIcon,
+} from '@phosphor-icons/react';
+
+interface AddRolesProps {
+ roleToEdit?: RoleItem | null;
+ onBack: () => void;
+ onSaved: () => void;
+}
+
+export default function AddRoles({ roleToEdit, onBack, onSaved }: AddRolesProps) {
+ const [name, setName] = useState('');
+ const [description, setDescription] = useState('');
+ const [selectedPermissionIds, setSelectedPermissionIds] = useState>(new Set());
+ const [groupedPermissions, setGroupedPermissions] = useState>({});
+ const [loading, setLoading] = useState(false);
+ const [fetching, setFetching] = useState(true);
+ const [error, setError] = useState(null);
+
+ const isSystemRole = roleToEdit?.isSystem ?? false;
+
+ useEffect(() => {
+ loadPermissions();
+ }, [roleToEdit]);
+
+ const loadPermissions = async () => {
+ setFetching(true);
+ setError(null);
+ try {
+ const response = await RolesApi.getPermissions();
+ setGroupedPermissions(response.grouped || {});
+
+ if (roleToEdit) {
+ setName(roleToEdit.name);
+ setDescription(roleToEdit.description || '');
+
+ // Fetch full role details to get its permissions
+ const detailedRole = await RolesApi.getRoleById(roleToEdit.id);
+ const existingIds = (detailedRole.permissions || []).map((p) => p.id);
+ setSelectedPermissionIds(new Set(existingIds));
+ } else {
+ setName('');
+ setDescription('');
+ setSelectedPermissionIds(new Set());
+ }
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to load permissions list.');
+ } finally {
+ setFetching(false);
+ }
+ };
+
+ const togglePermission = (id: string) => {
+ if (isSystemRole) return;
+ setSelectedPermissionIds((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) {
+ next.delete(id);
+ } else {
+ next.add(id);
+ }
+ return next;
+ });
+ };
+
+ const toggleGroup = (_groupName: string, perms: PermissionItem[]) => {
+ if (isSystemRole) return;
+ const groupIds = perms.map((p) => p.id);
+ const allSelected = groupIds.every((id) => selectedPermissionIds.has(id));
+
+ setSelectedPermissionIds((prev) => {
+ const next = new Set(prev);
+ if (allSelected) {
+ groupIds.forEach((id) => next.delete(id));
+ } else {
+ groupIds.forEach((id) => next.add(id));
+ }
+ return next;
+ });
+ };
+
+ const selectAll = () => {
+ if (isSystemRole) return;
+ const allIds = Object.values(groupedPermissions).flatMap((perms) => perms.map((p) => p.id));
+ setSelectedPermissionIds(new Set(allIds));
+ };
+
+ const deselectAll = () => {
+ if (isSystemRole) return;
+ setSelectedPermissionIds(new Set());
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!name.trim()) {
+ setError('Role name is required.');
+ return;
+ }
+
+ if (selectedPermissionIds.size === 0) {
+ setError('Please select at least one permission for this role.');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const permissionIds = Array.from(selectedPermissionIds);
+ if (roleToEdit) {
+ await RolesApi.updateRole(roleToEdit.id, {
+ name: name.trim(),
+ description: description.trim(),
+ permissionIds,
+ });
+ } else {
+ await RolesApi.createRole({
+ name: name.trim(),
+ description: description.trim(),
+ permissionIds,
+ });
+ }
+ onSaved();
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to save role.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (fetching) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header bar */}
+
+
+
}
+ onClick={onBack}
+ className="!rounded-[10px] !h-[38px]"
+ >
+ Back
+
+
+
+ {roleToEdit ? `Edit Role: ${roleToEdit.name}` : 'Create Custom Role'}
+ {isSystemRole && (
+
+ System Protected
+
+ )}
+
+
+ Configure role metadata and assign module-specific action permissions.
+
+
+
+
+ {!isSystemRole && (
+
+
+ Select All
+
+
+ Deselect All
+
+
+ )}
+
+
+ {/* Error Alert */}
+ {error && (
+
setError(null)}
+ />
+ )}
+
+
+
+ );
+}
diff --git a/src/app/roles/components/RolesList.tsx b/src/app/roles/components/RolesList.tsx
new file mode 100644
index 0000000..3395581
--- /dev/null
+++ b/src/app/roles/components/RolesList.tsx
@@ -0,0 +1,263 @@
+import { useEffect, useState, useMemo } from 'react';
+import { RolesApi, type RoleItem } from '../RolesApi';
+import {
+ CustomInput,
+ CustomButton,
+ CustomAlertBanner,
+ CustomConfirmationModal,
+ Skeleton,
+} from '../../../components/custom';
+import {
+ ShieldCheckIcon,
+ PlusIcon,
+ LockKeyIcon,
+ UsersIcon,
+ KeyIcon,
+ PencilSimpleIcon,
+ TrashIcon,
+ MagnifyingGlassIcon,
+} from '@phosphor-icons/react';
+import Can from '../../../components/common/Can';
+
+interface RolesListProps {
+ onAddRole: () => void;
+ onEditRole: (role: RoleItem) => void;
+}
+
+export default function RolesList({ onAddRole, onEditRole }: RolesListProps) {
+ const [roles, setRoles] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [search, setSearch] = useState('');
+ const [deleteConfirmRole, setDeleteConfirmRole] = useState(null);
+ const [deleting, setDeleting] = useState(false);
+ const [successMsg, setSuccessMsg] = useState(null);
+
+ useEffect(() => {
+ loadRoles();
+ }, []);
+
+ const loadRoles = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await RolesApi.getRoles();
+ setRoles(data);
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to load roles');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!deleteConfirmRole) return;
+ setDeleting(true);
+ try {
+ await RolesApi.deleteRole(deleteConfirmRole.id);
+ setSuccessMsg(`Role "${deleteConfirmRole.name}" was deleted successfully.`);
+ setDeleteConfirmRole(null);
+ loadRoles();
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to delete role.');
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ const filteredRoles = useMemo(() => {
+ const q = search.toLowerCase().trim();
+ if (!q) return roles;
+ return roles.filter(
+ (r) =>
+ r.name.toLowerCase().includes(q) ||
+ r.slug.toLowerCase().includes(q) ||
+ (r.description || '').toLowerCase().includes(q)
+ );
+ }, [roles, search]);
+
+ if (loading) {
+ return (
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {error && (
+
setError(null)}
+ />
+ )}
+ {successMsg && (
+ setSuccessMsg(null)}
+ />
+ )}
+
+ {/* Top Action Bar */}
+
+
+ setSearch(e.target.value)}
+ leftIcon={ }
+ className="!bg-[#F3F6F5] !rounded-[10px] !h-[40px] !border !border-[#E5E7EB]"
+ containerClassName="!gap-0"
+ />
+
+
+
+ }
+ className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
+ onClick={onAddRole}
+ >
+ Create Role
+
+
+
+
+ {/* Roles Cards Grid */}
+
+ {filteredRoles.map((role) => (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {role.name}
+
+
+ {role.slug}
+
+
+
+
+ {role.isSystem ? (
+
+ System Default
+
+ ) : (
+
+ Custom
+
+ )}
+
+
+ {/* Description */}
+
+ {role.description || 'No description configured for this operational role.'}
+
+
+ {/* Metrics */}
+
+
+
+
+
+ Permissions
+
+
+ {role.permissionCount} rules
+
+
+
+
+
+
+
+
+ Users
+
+
+ {role.userCount} assigned
+
+
+
+
+
+
+ {/* Action Buttons */}
+
+
+ }
+ onClick={() => onEditRole(role)}
+ className="!rounded-[8px] !h-[34px]"
+ >
+ {role.isSystem ? 'View Permissions' : 'Edit Role'}
+
+
+
+ {!role.isSystem && (
+
+ }
+ onClick={() => setDeleteConfirmRole(role)}
+ className="!rounded-[8px] !h-[34px] !border-red-200 !text-red-600 hover:!bg-red-50"
+ >
+ Delete
+
+
+ )}
+
+
+ ))}
+
+
+ {/* Delete Confirmation Modal */}
+ setDeleteConfirmRole(null)}
+ onConfirm={handleDelete}
+ title="Delete Role"
+ description={`Are you sure you want to permanently delete the role "${deleteConfirmRole?.name}"? ${
+ deleteConfirmRole && deleteConfirmRole.userCount > 0
+ ? `Warning: ${deleteConfirmRole.userCount} user(s) are currently assigned to this role.`
+ : ''
+ }`}
+ confirmText={deleting ? 'Deleting...' : 'Confirm Delete'}
+ variant="danger"
+ isLoading={deleting}
+ />
+
+ );
+}
diff --git a/src/app/roles/index.tsx b/src/app/roles/index.tsx
new file mode 100644
index 0000000..2cb9333
--- /dev/null
+++ b/src/app/roles/index.tsx
@@ -0,0 +1,44 @@
+import { useState } from 'react';
+import RolesList from './components/RolesList';
+import AddRoles from './components/AddRoles';
+import type { RoleItem } from './RolesApi';
+
+export default function RolesManagementPage() {
+ const [view, setView] = useState<'list' | 'add' | 'edit'>('list');
+ const [selectedRole, setSelectedRole] = useState(null);
+
+ const handleAddRole = () => {
+ setSelectedRole(null);
+ setView('add');
+ };
+
+ const handleEditRole = (role: RoleItem) => {
+ setSelectedRole(role);
+ setView('edit');
+ };
+
+ const handleBack = () => {
+ setSelectedRole(null);
+ setView('list');
+ };
+
+ const handleSaved = () => {
+ setSelectedRole(null);
+ setView('list');
+ };
+
+ return (
+
+ {view === 'list' && (
+
+ )}
+ {(view === 'add' || view === 'edit') && (
+
+ )}
+
+ );
+}
diff --git a/src/app/simulation/components/SimulationTerminal.tsx b/src/app/simulation/components/SimulationTerminal.tsx
index a14e07f..e980dad 100644
--- a/src/app/simulation/components/SimulationTerminal.tsx
+++ b/src/app/simulation/components/SimulationTerminal.tsx
@@ -21,6 +21,7 @@ import type { SimulatedPassenger, SimulationFormState } from '../SimulationTypes
import { createRecoveryIncident } from '../../recoveryIncidents/RecoveryIncidentsApi';
import { getCategoryValues } from '../../configuration/masterData/MasterDataApi';
import { evaluateBatchSimulation } from '../SimulationApi';
+import Can from '../../../components/common/Can';
import {
CATEGORY_SIMULATION_CONFIGS,
type CategorySimulationConfig,
@@ -507,14 +508,20 @@ export default function SimulationTerminal() {
{/* Action Button using CustomButton */}
-
- Run Manifest Simulation
-
+
+ Execution permission (simulation:execute) required to run simulations.
+
+ }>
+
+ Run Manifest Simulation
+
+
@@ -562,17 +569,19 @@ export default function SimulationTerminal() {
/>
- }
- onClick={handleSaveAsRecovery}
- className="!border-[#1B9869] !text-[#1B9869] hover:!bg-[#1B9869]/5 !rounded-xl !px-4 !py-2 shrink-0 font-semibold text-xs"
- >
- Save as Recovery
-
+
+ }
+ onClick={handleSaveAsRecovery}
+ className="!border-[#1B9869] !text-[#1B9869] hover:!bg-[#1B9869]/5 !rounded-xl !px-4 !py-2 shrink-0 font-semibold text-xs"
+ >
+ Save as Recovery
+
+
{/* Dynamic Manifest Table */}
diff --git a/src/app/users/UsersApi.ts b/src/app/users/UsersApi.ts
new file mode 100644
index 0000000..fb474a9
--- /dev/null
+++ b/src/app/users/UsersApi.ts
@@ -0,0 +1,67 @@
+import { ApiClient } from '../api/ApiClient';
+import type { UserRole } from '../../context/AuthContext';
+
+export interface UserItem {
+ id: string;
+ email: string;
+ firstName: string;
+ lastName?: string;
+ fullName: string;
+ roleId: string;
+ role: UserRole;
+ tenantId?: string | null;
+ isActive: boolean;
+ isSystem: boolean;
+ permissions: string[];
+ lastLoginAt?: string | null;
+ createdAt: string;
+ updatedAt?: string;
+}
+
+export interface CreateUserPayload {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName?: string;
+ roleId: string;
+ isActive?: boolean;
+}
+
+export interface UpdateUserPayload {
+ firstName?: string;
+ lastName?: string;
+ roleId?: string;
+ isActive?: boolean;
+}
+
+export const UsersApi = {
+ getUsers: async (search?: string, roleId?: string): Promise => {
+ return ApiClient.get('/users', {
+ params: { search, roleId },
+ });
+ },
+
+ getUserById: async (id: string): Promise => {
+ return ApiClient.get(`/users/${id}`);
+ },
+
+ createUser: async (payload: CreateUserPayload): Promise => {
+ return ApiClient.post('/users', payload);
+ },
+
+ updateUser: async (id: string, payload: UpdateUserPayload): Promise => {
+ return ApiClient.patch(`/users/${id}`, payload);
+ },
+
+ resetPassword: async (id: string, newPassword: string): Promise<{ success: boolean; message: string }> => {
+ return ApiClient.post(`/users/${id}/reset-password`, {
+ newPassword,
+ });
+ },
+
+ deleteUser: async (id: string): Promise<{ success: boolean; message: string }> => {
+ return ApiClient.delete(`/users/${id}`);
+ },
+};
+
+export default UsersApi;
diff --git a/src/app/users/components/ResetPasswordModal.tsx b/src/app/users/components/ResetPasswordModal.tsx
new file mode 100644
index 0000000..f469068
--- /dev/null
+++ b/src/app/users/components/ResetPasswordModal.tsx
@@ -0,0 +1,110 @@
+import React, { useState } from 'react';
+import { UsersApi, type UserItem } from '../UsersApi';
+import {
+ CustomModal,
+ CustomInput,
+ CustomButton,
+ CustomAlertBanner,
+} from '../../../components/custom';
+import { LockKeyIcon, KeyIcon } from '@phosphor-icons/react';
+
+interface ResetPasswordModalProps {
+ user: UserItem | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onSuccess: (email: string) => void;
+}
+
+export default function ResetPasswordModal({
+ user,
+ isOpen,
+ onClose,
+ onSuccess,
+}: ResetPasswordModalProps) {
+ const [newPassword, setNewPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ if (!isOpen || !user) return null;
+
+ const handleSubmit = async (e?: React.FormEvent) => {
+ if (e) e.preventDefault();
+ if (!newPassword || newPassword.length < 8) {
+ setError('New password must be at least 8 characters long.');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ await UsersApi.resetPassword(user.id, newPassword);
+ onSuccess(user.email);
+ onClose();
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to reset password.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Cancel
+
+ }
+ loading={loading}
+ disabled={loading}
+ onClick={handleSubmit}
+ className="!rounded-[10px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
+ >
+ Update Password
+
+
+ }
+ >
+
+ {error && (
+
setError(null)}
+ />
+ )}
+
+
+ Setting a new password will immediately revoke active tokens and require the user to sign in with the new credentials.
+
+
+
+
+
+ );
+}
diff --git a/src/app/users/components/UserModal.tsx b/src/app/users/components/UserModal.tsx
new file mode 100644
index 0000000..f36f2a8
--- /dev/null
+++ b/src/app/users/components/UserModal.tsx
@@ -0,0 +1,264 @@
+import React, { useEffect, useState } from 'react';
+import { UsersApi, type UserItem } from '../UsersApi';
+import { RolesApi, type RoleItem } from '../../roles/RolesApi';
+import {
+ CustomModal,
+ CustomInput,
+ CustomDropdown,
+ CustomSwitch,
+ CustomButton,
+ CustomAlertBanner,
+} from '../../../components/custom';
+import {
+ UserIcon,
+ EnvelopeSimpleIcon,
+ LockKeyIcon,
+ ShieldCheckIcon,
+ FloppyDiskIcon,
+} from '@phosphor-icons/react';
+
+interface UserModalProps {
+ userToEdit?: UserItem | null;
+ isOpen: boolean;
+ onClose: () => void;
+ onSaved: () => void;
+}
+
+export default function UserModal({
+ userToEdit,
+ isOpen,
+ onClose,
+ onSaved,
+}: UserModalProps) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [roleId, setRoleId] = useState('');
+ const [isActive, setIsActive] = useState(true);
+
+ const [roles, setRoles] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (isOpen) {
+ loadRoles();
+ if (userToEdit) {
+ setEmail(userToEdit.email);
+ setFirstName(userToEdit.firstName);
+ setLastName(userToEdit.lastName || '');
+ setRoleId(userToEdit.roleId);
+ setIsActive(userToEdit.isActive);
+ setPassword('');
+ } else {
+ setEmail('');
+ setPassword('');
+ setFirstName('');
+ setLastName('');
+ setRoleId('');
+ setIsActive(true);
+ }
+ setError(null);
+ }
+ }, [isOpen, userToEdit]);
+
+ const loadRoles = async () => {
+ try {
+ const data = await RolesApi.getRoles();
+ setRoles(data);
+ if (!userToEdit && data.length > 0 && !roleId) {
+ setRoleId(data[0].id);
+ }
+ } catch (e) {
+ console.error('Failed to load roles in user modal:', e);
+ }
+ };
+
+ const isSystemAdmin = userToEdit?.isSystem ?? false;
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+
+ if (!firstName.trim()) {
+ setError('First name is required.');
+ return;
+ }
+
+ if (!userToEdit && !email.trim()) {
+ setError('Email address is required.');
+ return;
+ }
+
+ if (!userToEdit && (!password || password.length < 8)) {
+ setError('Password must be at least 8 characters long.');
+ return;
+ }
+
+ if (!roleId) {
+ setError('Please assign an operational role.');
+ return;
+ }
+
+ setLoading(true);
+
+ try {
+ if (userToEdit) {
+ await UsersApi.updateUser(userToEdit.id, {
+ firstName: firstName.trim(),
+ lastName: lastName.trim() || undefined,
+ roleId,
+ isActive,
+ });
+ } else {
+ await UsersApi.createUser({
+ email: email.trim().toLowerCase(),
+ password,
+ firstName: firstName.trim(),
+ lastName: lastName.trim() || undefined,
+ roleId,
+ isActive,
+ });
+ }
+ onSaved();
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to save user account.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const roleOptions = roles.map((r) => ({
+ label: `${r.name}${r.isSystem ? ' (System Default)' : ''}`,
+ value: r.id,
+ }));
+
+ return (
+
+
+ Cancel
+
+ }
+ loading={loading}
+ disabled={loading}
+ onClick={handleSubmit}
+ className="!rounded-[10px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
+ >
+ {userToEdit ? 'Save Changes' : 'Provision User'}
+
+
+ }
+ >
+
+ {error && (
+
setError(null)}
+ />
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/users/components/UsersList.tsx b/src/app/users/components/UsersList.tsx
new file mode 100644
index 0000000..3c92652
--- /dev/null
+++ b/src/app/users/components/UsersList.tsx
@@ -0,0 +1,407 @@
+import { useState, useEffect, useCallback, useMemo } from 'react';
+import { UsersApi, type UserItem } from '../UsersApi';
+import {
+ CustomTable,
+ CustomInput,
+ CustomButton,
+ CustomStatus,
+ CustomCheckBox,
+ Skeleton,
+ CustomActionMenu,
+ CustomActionItem,
+ CustomAlertBanner,
+ CustomConfirmationModal,
+} from '../../../components/custom';
+import type { Column } from '../../../components/custom/CustomTable';
+import {
+ PencilSimpleIcon,
+ TrashIcon,
+ KeyIcon,
+ PlusIcon,
+ MagnifyingGlassIcon,
+ LockKeyIcon,
+} from '@phosphor-icons/react';
+import UserModal from './UserModal';
+import ResetPasswordModal from './ResetPasswordModal';
+import Can from '../../../components/common/Can';
+import { formatDate } from '../../../utils/formatDate';
+
+const PAGE_SIZE = 10;
+
+function HeaderLabel({
+ text,
+ rightIcon,
+}: {
+ text: string;
+ rightIcon?: React.ReactNode;
+}) {
+ return (
+
+
+ {text}
+
+ {rightIcon && {rightIcon} }
+
+ );
+}
+
+function PrimaryText({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+function SecondaryText({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+function BadgeLabel({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+export default function UsersList() {
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [successMsg, setSuccessMsg] = useState(null);
+
+ // Pagination & Search
+ const [currentPage, setCurrentPage] = useState(1);
+ const [search, setSearch] = useState('');
+
+ // Selection
+ const [selectedIds, setSelectedIds] = useState>(new Set());
+
+ // Modals
+ const [userModalOpen, setUserModalOpen] = useState(false);
+ const [userToEdit, setUserToEdit] = useState(null);
+
+ const [resetModalOpen, setResetModalOpen] = useState(false);
+ const [userForReset, setUserForReset] = useState(null);
+
+ const [deleteConfirmUser, setDeleteConfirmUser] = useState(null);
+ const [deleting, setDeleting] = useState(false);
+
+ const fetchUsers = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await UsersApi.getUsers();
+ setUsers(data);
+ } catch (err: any) {
+ console.error('Failed to load users:', err);
+ setError(err?.response?.data?.message || 'Failed to load user accounts.');
+ setUsers([]);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchUsers();
+ }, [fetchUsers]);
+
+ // Handlers
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ };
+
+ const handleSearchChange = (val: string) => {
+ setSearch(val);
+ setCurrentPage(1);
+ };
+
+ const toggleSelectAll = () => {
+ if (selectedIds.size === users.length && users.length > 0) {
+ setSelectedIds(new Set());
+ } else {
+ setSelectedIds(new Set(users.map((u) => u.id)));
+ }
+ };
+
+ const toggleSelectOne = (id: string) => {
+ const next = new Set(selectedIds);
+ if (next.has(id)) {
+ next.delete(id);
+ } else {
+ next.add(id);
+ }
+ setSelectedIds(next);
+ };
+
+ const handleCreateUser = () => {
+ setUserToEdit(null);
+ setUserModalOpen(true);
+ };
+
+ const handleEditUser = (user: UserItem) => {
+ setUserToEdit(user);
+ setUserModalOpen(true);
+ };
+
+ const handleResetPassword = (user: UserItem) => {
+ setUserForReset(user);
+ setResetModalOpen(true);
+ };
+
+ const handleDelete = async () => {
+ if (!deleteConfirmUser) return;
+ setDeleting(true);
+ try {
+ await UsersApi.deleteUser(deleteConfirmUser.id);
+ setSuccessMsg(`User "${deleteConfirmUser.email}" was removed successfully.`);
+ setDeleteConfirmUser(null);
+ fetchUsers();
+ } catch (err: any) {
+ setError(err?.response?.data?.message || 'Failed to delete user.');
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ const handleSaved = () => {
+ setUserModalOpen(false);
+ setSuccessMsg('User account updated successfully.');
+ fetchUsers();
+ };
+
+ const handleResetSuccess = (email: string) => {
+ setSuccessMsg(`Password for ${email} was reset successfully.`);
+ };
+
+ // Filtered & Paginated Data
+ const filteredUsers = useMemo(() => {
+ const q = search.toLowerCase().trim();
+ if (!q) return users;
+ return users.filter(
+ (u) =>
+ u.fullName.toLowerCase().includes(q) ||
+ u.email.toLowerCase().includes(q) ||
+ (u.role?.name || '').toLowerCase().includes(q)
+ );
+ }, [users, search]);
+
+ const totalItems = filteredUsers.length;
+ const totalPages = Math.ceil(totalItems / PAGE_SIZE) || 1;
+ const startIndex = totalItems > 0 ? (currentPage - 1) * PAGE_SIZE + 1 : 0;
+ const endIndex = Math.min(currentPage * PAGE_SIZE, totalItems);
+
+ const paginatedUsers = useMemo(() => {
+ const start = (currentPage - 1) * PAGE_SIZE;
+ return filteredUsers.slice(start, start + PAGE_SIZE);
+ }, [filteredUsers, currentPage]);
+
+ // Columns definition
+ const columns: Column[] = [
+ {
+ header: (
+ 0 && selectedIds.size === users.length}
+ onChange={toggleSelectAll}
+ />
+ ),
+ className: 'w-[40px] pr-0',
+ accessor: (row) => (
+ toggleSelectOne(row.id)}
+ onClick={(e) => e.stopPropagation()}
+ />
+ ),
+ },
+ {
+ header: ,
+ accessor: (row) => (
+
+
+
+ {row.isSystem && (
+
+
+ System Admin
+
+ )}
+
+
+
+ ),
+ },
+ {
+ header: ,
+ accessor: (row) => ,
+ },
+ {
+ header: ,
+ accessor: (row) => (
+
+ ),
+ },
+ {
+ header: ,
+ accessor: (row) => (
+
+ ),
+ },
+ {
+ header: ,
+ accessor: (row) => ,
+ },
+ {
+ header: ,
+ accessor: (row) => (
+
+
+
+ handleEditUser(row)}
+ icon={ }
+ >
+ Edit User
+
+
+
+
+ handleResetPassword(row)}
+ icon={ }
+ >
+ Reset Password
+
+
+
+ {!row.isSystem && (
+
+ }
+ onClick={() => setDeleteConfirmUser(row)}
+ >
+ Delete User
+
+
+ )}
+
+
+ ),
+ },
+ ];
+
+ if (loading) {
+ return (
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {error && (
+
setError(null)}
+ />
+ )}
+ {successMsg && (
+ setSuccessMsg(null)}
+ />
+ )}
+
+ {/* Table Section */}
+
+ columns={columns}
+ data={paginatedUsers}
+ leftHeaderActions={
+
+ handleSearchChange(e.target.value)}
+ leftIcon={ }
+ className="!bg-[#F3F6F5] !rounded-[10px] !h-[40px] !border !border-[#E5E7EB]"
+ containerClassName="!gap-0"
+ />
+
+ }
+ rightHeaderActions={
+
+ }
+ className="!rounded-[10px] !gap-[8px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
+ onClick={handleCreateUser}
+ >
+ Provision User
+
+
+ }
+ currentPage={currentPage}
+ totalPages={totalPages}
+ totalItems={totalItems}
+ startIndex={startIndex}
+ endIndex={endIndex}
+ onPageChange={handlePageChange}
+ itemName="Users"
+ rowClassName={() => 'bg-white border-b border-gray-100 hover:bg-gray-50/60'}
+ />
+
+ {/* Create / Edit User Modal */}
+ setUserModalOpen(false)}
+ onSaved={handleSaved}
+ />
+
+ {/* Reset Password Modal */}
+ setResetModalOpen(false)}
+ onSuccess={handleResetSuccess}
+ />
+
+ {/* Confirmation Modal */}
+ setDeleteConfirmUser(null)}
+ onConfirm={handleDelete}
+ title="Delete User Account"
+ description={`Are you sure you want to permanently remove user "${deleteConfirmUser?.email}"? All active sessions will be invalidated.`}
+ confirmText={deleting ? 'Deleting...' : 'Confirm Delete'}
+ variant="danger"
+ isLoading={deleting}
+ />
+
+ );
+}
diff --git a/src/app/users/index.tsx b/src/app/users/index.tsx
new file mode 100644
index 0000000..6c0e63c
--- /dev/null
+++ b/src/app/users/index.tsx
@@ -0,0 +1,9 @@
+import UsersList from './components/UsersList';
+
+export default function UsersManagementPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/common/Can.tsx b/src/components/common/Can.tsx
new file mode 100644
index 0000000..0cec4c9
--- /dev/null
+++ b/src/components/common/Can.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { useAuth } from '../../context/AuthContext';
+
+interface CanProps {
+ permission?: string;
+ permissions?: string[];
+ matchAny?: boolean;
+ fallback?: React.ReactNode;
+ children: React.ReactNode;
+}
+
+export const Can: React.FC = ({
+ permission,
+ permissions,
+ matchAny = true,
+ fallback = null,
+ children,
+}) => {
+ const { hasPermission, hasAnyPermission, hasAllPermissions } = useAuth();
+
+ let isAllowed = false;
+
+ if (permission) {
+ isAllowed = hasPermission(permission);
+ } else if (permissions && permissions.length > 0) {
+ isAllowed = matchAny
+ ? hasAnyPermission(permissions)
+ : hasAllPermissions(permissions);
+ } else {
+ isAllowed = true;
+ }
+
+ if (!isAllowed) {
+ return <>{fallback}>;
+ }
+
+ return <>{children}>;
+};
+
+export default Can;
diff --git a/src/components/common/ProtectedRoute.tsx b/src/components/common/ProtectedRoute.tsx
new file mode 100644
index 0000000..0b3d526
--- /dev/null
+++ b/src/components/common/ProtectedRoute.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { Navigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+import { ShieldAlert, Shield } from 'lucide-react';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+ permission?: string;
+ permissions?: string[];
+ matchAny?: boolean;
+}
+
+export const ProtectedRoute: React.FC = ({
+ children,
+ permission,
+ permissions,
+ matchAny = true,
+}) => {
+ const { isAuthenticated, isLoading, hasPermission, hasAnyPermission, hasAllPermissions } = useAuth();
+ const location = useLocation();
+
+ if (isLoading) {
+ return (
+
+
+
+ Authenticating Aero Resolve Session...
+
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // Check required permissions
+ let isAuthorized = true;
+ if (permission) {
+ isAuthorized = hasPermission(permission);
+ } else if (permissions && permissions.length > 0) {
+ isAuthorized = matchAny ? hasAnyPermission(permissions) : hasAllPermissions(permissions);
+ }
+
+ if (!isAuthorized) {
+ return (
+
+
+
+
+
Access Restricted
+
+ Your current role does not have the required security clearance to access this operational module.
+
+
+ Required Clearance: {permission || permissions?.join(', ')}
+
+
+ );
+ }
+
+ return <>{children}>;
+};
+
+export default ProtectedRoute;
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..2ee07a4
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,142 @@
+import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
+import { ApiClient } from '../app/api/ApiClient';
+
+export interface UserRole {
+ id: string;
+ name: string;
+ slug: string;
+ description?: string;
+ isSystem: boolean;
+}
+
+export interface AuthUser {
+ id: string;
+ email: string;
+ firstName: string;
+ lastName?: string;
+ fullName: string;
+ roleId: string;
+ role: UserRole;
+ tenantId?: string | null;
+ isActive: boolean;
+ isSystem: boolean;
+ permissions: string[];
+ lastLoginAt?: string;
+ createdAt: string;
+}
+
+interface AuthContextType {
+ user: AuthUser | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ login: (email: string, password: string) => Promise;
+ logout: () => Promise;
+ refreshProfile: () => Promise;
+ hasPermission: (permissionCode: string) => boolean;
+ hasAnyPermission: (permissionCodes: string[]) => boolean;
+ hasAllPermissions: (permissionCodes: string[]) => boolean;
+}
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Verify active session on app boot
+ const refreshProfile = useCallback(async () => {
+ try {
+ const profile = await ApiClient.get('/auth/me');
+ setUser(profile);
+ } catch {
+ setUser(null);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refreshProfile();
+ }, [refreshProfile]);
+
+ const login = async (email: string, password: string): Promise => {
+ setIsLoading(true);
+ try {
+ const response = await ApiClient.post(
+ '/auth/login',
+ { email, password }
+ );
+ setUser(response.user);
+ return response.user;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const logout = async () => {
+ try {
+ await ApiClient.post('/auth/logout');
+ } catch (e) {
+ console.warn('Logout API error:', e);
+ } finally {
+ setUser(null);
+ window.location.href = '/login';
+ }
+ };
+
+ const hasPermission = useCallback(
+ (permissionCode: string): boolean => {
+ if (!user) return false;
+ // Super admin has all permissions
+ if (user.role?.slug === 'super_admin' || user.isSystem) return true;
+ return user.permissions?.includes(permissionCode) ?? false;
+ },
+ [user]
+ );
+
+ const hasAnyPermission = useCallback(
+ (permissionCodes: string[]): boolean => {
+ if (!user) return false;
+ if (user.role?.slug === 'super_admin' || user.isSystem) return true;
+ return permissionCodes.some((code) => user.permissions?.includes(code));
+ },
+ [user]
+ );
+
+ const hasAllPermissions = useCallback(
+ (permissionCodes: string[]): boolean => {
+ if (!user) return false;
+ if (user.role?.slug === 'super_admin' || user.isSystem) return true;
+ return permissionCodes.every((code) => user.permissions?.includes(code));
+ },
+ [user]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+export default AuthContext;
diff --git a/src/layout/AppHeader.tsx b/src/layout/AppHeader.tsx
index 2b11f2e..ffd401d 100644
--- a/src/layout/AppHeader.tsx
+++ b/src/layout/AppHeader.tsx
@@ -10,6 +10,9 @@ const PAGE_META: Record = {
'/action-builder': { title: 'Configuration - Action Builder', subtitle: 'Configure dynamic categories, action types, metadata fields, and dynamic user form workflows.' },
'/config': { title: 'Configuration', subtitle: 'Manage dynamic action workflows, categories, metadata fields, and system master data.' },
'/recovery': { title: 'Recovery Incidents', subtitle: 'Operational workspace for managing passenger disruption cases.' },
+ '/users': { title: 'User Management', subtitle: 'Provision accounts, assign role permissions, and control tenant access.' },
+ '/roles': { title: 'Roles & Permissions', subtitle: 'Configure role-based access control and granular permission matrices.' },
+ '/audit-logs': { title: 'Audit Trail & Compliance', subtitle: 'Immutable chronological event ledger for operational compliance.' },
};
interface AppHeaderProps {
diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx
index 225ba05..1d24c5b 100644
--- a/src/layout/AppSidebar.tsx
+++ b/src/layout/AppSidebar.tsx
@@ -11,17 +11,28 @@ import {
GearIcon,
ClockCounterClockwiseIcon,
CaretDoubleRightIcon,
+ ShieldCheckIcon,
+ LockKeyIcon,
} from "@phosphor-icons/react";
-import { ShieldCheckIcon } from "lucide-react";
+import { useAuth } from "../context/AuthContext";
-const NAV_ITEMS = [
- { label: "Dashboard", path: "/", icon: SquaresFourIcon },
- { label: "Simulation Engine", path: "/simulation", icon: FadersIcon },
- { label: "Recovery Incidents", path: "/recovery", icon: ArrowsClockwiseIcon },
- { label: "Cohort Management", path: "/cohorts", icon: UsersFourIcon },
- { label: "Policy Engine", path: "/policy-engine", icon: ShieldCheckIcon },
- { label: "Configuration", path: "/config", icon: GearIcon },
- { label: "Audit Logs", path: "/audit-logs", icon: ClockCounterClockwiseIcon },
+interface NavItem {
+ label: string;
+ path: string;
+ icon: any;
+ permission?: string;
+}
+
+const NAV_ITEMS: NavItem[] = [
+ { label: "Dashboard", path: "/", icon: SquaresFourIcon, permission: "dashboard:view" },
+ { label: "Simulation Engine", path: "/simulation", icon: FadersIcon, permission: "simulation:view" },
+ { label: "Recovery Incidents", path: "/recovery", icon: ArrowsClockwiseIcon, permission: "recovery:view" },
+ { label: "Cohort Management", path: "/cohorts", icon: UsersFourIcon, permission: "cohorts:view" },
+ { label: "Policy Engine", path: "/policy-engine", icon: ShieldCheckIcon, permission: "policy_engine:view" },
+ { label: "Configuration", path: "/config", icon: GearIcon, permission: "config:view" },
+ { label: "User Management", path: "/users", icon: UsersFourIcon, permission: "users:view" },
+ { label: "Roles & Permissions", path: "/roles", icon: LockKeyIcon, permission: "roles:view" },
+ { label: "Audit Logs", path: "/audit-logs", icon: ClockCounterClockwiseIcon, permission: "audit_logs:view" },
];
interface AppSidebarProps {
@@ -32,6 +43,19 @@ interface AppSidebarProps {
export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
const location = useLocation();
const [isCollapsed, setIsCollapsed] = useState(false);
+ const { user, logout, hasPermission } = useAuth();
+
+ const visibleNavItems = NAV_ITEMS.filter((item) => {
+ if (!item.permission) return true;
+ return hasPermission(item.permission);
+ });
+
+ const initials = user
+ ? `${user.firstName?.[0] || ''}${user.lastName?.[0] || ''}`.toUpperCase() || user.email?.[0]?.toUpperCase() || 'AD'
+ : 'AD';
+
+ const userDisplayName = user?.fullName || `${user?.firstName || ''} ${user?.lastName || ''}`.trim() || user?.email || 'Admin Demo';
+ const roleDisplayName = user?.role?.name || (user?.isSystem ? 'System Administrator' : 'User');
return (
<>
@@ -95,7 +119,7 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
- {NAV_ITEMS.map((item) => {
+ {visibleNavItems.map((item) => {
const isActive =
location.pathname === item.path ||
(item.path !== "/" && location.pathname.startsWith(item.path));
@@ -141,8 +165,9 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) {
className={`relative z-10 flex flex-col ${isCollapsed ? "gap-2 w-full" : "gap-0.5"}`}
>
-
+
+ {initials}
+
{!isCollapsed && (
- Admin Demo
+ {userDisplayName}
- System Administrator
+ {roleDisplayName}
)}