diff --git a/package-lock.json b/package-lock.json index 287e27e..3103d92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@tailwindcss/vite": "^4.3.0", "axios": "^1.18.1", "framer-motion": "^12.40.0", - "lucide-react": "^1.23.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-icons": "^5.6.0", @@ -2842,15 +2841,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", - "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index bcba6c0..275d26b 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@tailwindcss/vite": "^4.3.0", "axios": "^1.18.1", "framer-motion": "^12.40.0", - "lucide-react": "^1.23.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-icons": "^5.6.0", @@ -48,4 +47,4 @@ "typescript-eslint": "^8.59.2", "vite": "^8.0.12" } -} +} \ No newline at end of file diff --git a/src/app/authentication/components/Sigin.tsx b/src/app/authentication/components/Sigin.tsx index bc54285..50e66b4 100644 --- a/src/app/authentication/components/Sigin.tsx +++ b/src/app/authentication/components/Sigin.tsx @@ -1,10 +1,232 @@ -function HomePage() { - return ( -
-

Home Page

-

Welcome to the home page. Use the navigation links to switch pages.

-
- ) +import React, { useState } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../../context/AuthContext'; +import { + CustomInput, + CustomButton, + CustomCheckBox, + CustomAlertBanner, +} from '../../../components/custom'; +import { + EnvelopeSimpleIcon, + LockKeyIcon, + ArrowRightIcon, + SparkleIcon, + ShieldCheckIcon, + InfoIcon, +} from '@phosphor-icons/react'; + +interface SiginProps { + onSuccess?: () => void; + className?: string; } -export default HomePage +export default function Sigin({ onSuccess, className = '' }: SiginProps) { + const { login } = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showForgotNotice, setShowForgotNotice] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!email.trim() || !password.trim()) { + setError('Please provide both your enterprise email and password.'); + return; + } + + setError(null); + setLoading(true); + + try { + await login(email.trim(), password); + if (onSuccess) { + onSuccess(); + } else { + 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 and network access.'; + setError(msg); + } finally { + setLoading(false); + } + }; + + const handleQuickFill = () => { + setEmail('admin@aeroresolve.com'); + setPassword('Password@123'); + setError(null); + }; + + return ( +
+ {/* Brand Header */} +
+
+
+ + + +
+
+
+
+ + Aero Resolve + + + Enterprise + +
+

Disruption Management & Recovery

+
+
+ +
+

+ Sign In to Terminal +

+

+ Enter your enterprise credentials to access operational control. +

+
+
+ + {/* Demo Credentials Quick Fill Helper */} +
+
+
+ +
+
+ Super Admin Demo + admin@aeroresolve.com +
+
+ +
+ + {/* Error Alert */} + {error && ( + setError(null)} + autoClose={false} + /> + )} + + {/* Forgot Password Modal / Alert Notice */} + {showForgotNotice && ( +
+
+ +

+ For security compliance, password resets require airline system administrator approval. Contact your IT operations lead at support@aeroresolve.com. +

+
+ +
+ )} + + {/* Form */} +
+
+ setEmail(e.target.value)} + placeholder="e.g. name@aeroresolve.com" + leftIcon={} + className="!h-[44px] !rounded-[10px]" + /> +
+ +
+ setPassword(e.target.value)} + placeholder="••••••••••••" + leftIcon={} + className="!h-[44px] !rounded-[10px]" + /> +
+ + {/* Remember me & Forgot Password */} +
+ setRememberMe(e.target.checked)} + /> + + +
+ + {/* Submit Button */} +
+ } + className="!w-full !h-[46px] !rounded-[12px] !bg-[#1B9869] hover:!bg-[#14704E] font-bold text-sm shadow-sm transition-all" + > + {loading ? 'Authenticating...' : 'Sign In to Terminal'} + +
+
+ + {/* Security Footer */} +
+ + Secured via Role-Based Access Control (RBAC) +
+
+ ); +} diff --git a/src/app/authentication/index.tsx b/src/app/authentication/index.tsx index 3c0a7ef..9cd7a0e 100644 --- a/src/app/authentication/index.tsx +++ b/src/app/authentication/index.tsx @@ -1,90 +1,62 @@ -import React, { useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import Sigin from './components/Sigin'; import { - Lock, - Mail, - Eye, - EyeOff, - ShieldCheck, - Plane, - KeyRound, - Sparkles, - ArrowRight, - AlertCircle, -} from 'lucide-react'; + ShieldCheckIcon, + AirplaneTiltIcon, + CheckCircleIcon, +} from '@phosphor-icons/react'; export default function LoginPage() { - const { login, isAuthenticated } = useAuth(); + const { 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 already authenticated, redirect to destination or root + 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 */} -
-
-
+
+ {/* Background glow and subtle aviation radar circles */} +
+
+
- {/* Top Logo */} -
-
- + {/* Top Brand Logo */} +
+
+ + + +
-

- AERO RESOLVE - +
+

+ AERO RESOLVE +

+ Enterprise -

+

Aviation Passenger Recovery & Compensation Intelligence

@@ -92,161 +64,63 @@ export default function LoginPage() {
{/* Hero Central Content */} -
-
- - - Secure Role-Based Access Control (RBAC) +
+
+ + + Multi-Jurisdiction Regulatory Decision Framework
-

+

Next-Generation Flight Disruption Orchestration

-

- Automate passenger segmentation, multi-jurisdiction regulatory compensation, goodwill - policies, and live manifest recovery simulations in real time. +

+ Automate passenger cohort segmentation, statutory compensation (EU261, US DOT, APPR, UK261), + goodwill policies, and live manifest recovery simulation in real time.

+ {/* Operational Feature Badges */} +
+
+ + + Dynamic Policy Rule Engine + +
+
+ + + Real-Time Incident Recovery + +
+
+ + {/* Metrics Row */}
- 99.98% + 99.98% Uptime Reliability
- < 150ms - Policy Engine Evaluation + < 150ms + Evaluation Latency
{/* Bottom Trust Footer */} -
+
Aero Resolve Operating System v1.0 Aerospace High-Security Protocol
{/* Right / Login Form Column */} -
-
- {/* Header Mobile / Title */} -
-
-
- -
- Aero Resolve -
- -

- Sign In to Terminal -

-

- Enter your credentials to access operations management. -

-
- - {/* Quick Demo Fill Helper */} -
-
- -
- Super Admin Demo Credentials - admin@aeroresolve.com -
-
- -
- - {/* Error Banner */} - {error && ( -
- - {error} -
- )} - - {/* Form */} -
-
- -
-
- -
- setEmail(e.target.value)} - placeholder="name@aeroresolve.com" - className="w-full pl-10 pr-4 py-3 bg-slate-900/90 border border-slate-700/80 rounded-xl text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-teal-500 transition-all" - /> -
-
- -
-
- - - Forgot Password? - -
-
-
- -
- setPassword(e.target.value)} - placeholder="••••••••••••" - className="w-full pl-10 pr-11 py-3 bg-slate-900/90 border border-slate-700/80 rounded-xl text-white text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-teal-500 transition-all" - /> - -
-
- - -
- - {/* Security badge footer */} -
- - Encrypted with HTTP-Only Cookie Authentication -
+
+
+
diff --git a/src/app/recoveryIncidents/tabs/index.tsx b/src/app/recoveryIncidents/tabs/index.tsx index bd6e5ab..4322cc0 100644 --- a/src/app/recoveryIncidents/tabs/index.tsx +++ b/src/app/recoveryIncidents/tabs/index.tsx @@ -1,12 +1,11 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon } from '@phosphor-icons/react'; +import { User, Checks, X, ClockCounterClockwiseIcon, ArrowLeftIcon, ArrowsClockwiseIcon, AirplaneTiltIcon, SparkleIcon } from '@phosphor-icons/react'; import { CustomButton, CustomTabs, CustomBackButton, CustomStatus, Skeleton, CustomAlertBanner } from '../../../components/custom'; import SummaryTab from './SummaryTab'; import CaseDetailsTab from './CaseDetailsTab'; import RecoveryPlanTab from './RecoveryPlanTab'; import AuditTrailTab from './AuditTrailTab'; -import { SparkleIcon } from 'lucide-react'; import { getRecoveryIncident, updateIncidentStatus, reRunPolicyEngine } from '../RecoveryIncidentsApi'; import type { RecoveryIncident } from '../RecoveryIncidentsTypes'; @@ -254,7 +253,7 @@ export default function RecoveryIncidentTabs() { {/* Sidebar Content */}
- +

AI RECOMMENDATION

diff --git a/src/app/roles/components/AddRoles.tsx b/src/app/roles/components/AddRoles.tsx index 3ce8ea6..52c5d1f 100644 --- a/src/app/roles/components/AddRoles.tsx +++ b/src/app/roles/components/AddRoles.tsx @@ -4,15 +4,15 @@ import { CustomInput, CustomButton, CustomCheckBox, + CustomTextArea, CustomAlertBanner, + CustomSuccessModal, Skeleton, } from '../../../components/custom'; import { ShieldCheckIcon, - LockKeyIcon, ArrowLeftIcon, - FloppyDiskIcon, - SquaresFourIcon, + LockKeyIcon, } from '@phosphor-icons/react'; interface AddRolesProps { @@ -29,6 +29,8 @@ export default function AddRoles({ roleToEdit, onBack, onSaved }: AddRolesProps) const [loading, setLoading] = useState(false); const [fetching, setFetching] = useState(true); const [error, setError] = useState(null); + const [showSuccessModal, setShowSuccessModal] = useState(false); + const [successTitle, setSuccessTitle] = useState(''); const isSystemRole = roleToEdit?.isSystem ?? false; @@ -103,8 +105,8 @@ export default function AddRoles({ roleToEdit, onBack, onSaved }: AddRolesProps) setSelectedPermissionIds(new Set()); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); if (!name.trim()) { setError('Role name is required.'); return; @@ -126,14 +128,16 @@ export default function AddRoles({ roleToEdit, onBack, onSaved }: AddRolesProps) description: description.trim(), permissionIds, }); + setSuccessTitle('Role Updated Successfully.'); } else { await RolesApi.createRole({ name: name.trim(), description: description.trim(), permissionIds, }); + setSuccessTitle('Role Created Successfully.'); } - onSaved(); + setShowSuccessModal(true); } catch (err: any) { setError(err?.response?.data?.message || 'Failed to save role.'); } finally { @@ -141,215 +145,312 @@ export default function AddRoles({ roleToEdit, onBack, onSaved }: AddRolesProps) } }; + const CardHeader = ({ icon: Icon, title }: { icon: any; title: string }) => ( +
+
+ +
+

{title}

+
+ ); + if (fetching) { return ( -
- - - +
+ {/* ─── Header Skeleton ────────────────────────────────────────────── */} +
+
+
+ +
+
+ + +
+
+
+ + {/* ─── Body Content Skeleton ──────────────────────────────────────── */} +
+
+ {/* Role Information Card Skeleton */} +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + {/* Permissions Matrix Card Skeleton */} +
+
+
+ + +
+ +
+
+ +
+ + + +
+
+
+
+
); } return ( -
- {/* Header bar */} -
-
- } +
+ {/* ─── Header ────────────────────────────────────────────────────── */} +
+
+ +
+

+ {roleToEdit ? (isSystemRole ? 'View System Role' : 'Edit Role') : 'Create Custom Role'} +

+ Global Framework Registry
- - {!isSystemRole && ( -
- - Select All - - - Deselect All - -
- )}
- {/* Error Alert */} - {error && ( - setError(null)} - /> - )} - -
- {/* Role General Info */} -
-

- - General Information -

- -
- setName(e.target.value)} - placeholder="e.g. Flight Disruption Lead" - className="!h-[42px] !rounded-[10px]" + {/* ─── Body Content ──────────────────────────────────────────────── */} +
+
+ {/* Error Alert */} + {error && ( + setError(null)} /> + )} - setDescription(e.target.value)} - placeholder="e.g. Manages passenger goodwill offers and compensation evaluations" - className="!h-[42px] !rounded-[10px]" - /> -
-
+ {/* Role Information Card */} +
+ - {/* Permission Matrix */} -
-
-
-

- - Granular Permissions Matrix -

-

- Selected {selectedPermissionIds.size} permission(s) -

+
+
+ setName(e.target.value)} + placeholder="e.g. Flight Disruption Lead" + className="!h-11" + /> +
+
+ +
+ + {isSystemRole ? 'System Role (Immutable)' : 'Custom Role'} + +
+
+
+ +
+ setDescription(e.target.value)} + placeholder="e.g. Manages passenger goodwill offers and compensation evaluations" + className="!h-24 resize-none" + />
-
- {Object.entries(groupedPermissions).map(([groupName, perms]) => { - const groupIds = perms.map((p) => p.id); - const allGroupSelected = groupIds.every((id) => selectedPermissionIds.has(id)); + {/* Granular Permissions Matrix Card */} +
+
+
+ + + {selectedPermissionIds.size} Selected + +
- return ( -
- {/* Module header */} -
-
- toggleGroup(groupName, perms)} - /> - {groupName} -
- - {perms.filter((p) => selectedPermissionIds.has(p.id)).length} / {perms.length} selected - -
- - {/* Permission cards grid */} -
- {perms.map((perm) => { - const isSelected = selectedPermissionIds.has(perm.id); - return ( -
togglePermission(perm.id)} - className={`p-3 rounded-[10px] border transition-all cursor-pointer select-none flex items-start gap-3 ${ - isSelected - ? 'bg-emerald-50/70 border-emerald-300' - : 'bg-white border-gray-200 hover:border-gray-300' - } ${isSystemRole ? 'opacity-80 cursor-default' : ''}`} - > -
- togglePermission(perm.id)} - onClick={(e) => e.stopPropagation()} - /> -
-
- - {perm.name} - - - {perm.code} - - {perm.description && ( -

- {perm.description} -

- )} -
-
- ); - })} -
+ {!isSystemRole && ( +
+ + Select All + + + Deselect All +
- ); - })} + )} +
+ +
+ {Object.entries(groupedPermissions).map(([groupName, perms]) => { + const groupIds = perms.map((p) => p.id); + const allGroupSelected = groupIds.length > 0 && groupIds.every((id) => selectedPermissionIds.has(id)); + const selectedInGroupCount = perms.filter((p) => selectedPermissionIds.has(p.id)).length; + + return ( +
+ {/* Module Header */} +
+
+ toggleGroup(groupName, perms)} + /> +

{groupName}

+
+ + {selectedInGroupCount} / {perms.length} selected + +
+ + {/* Permission Cards Grid */} +
+ {perms.map((perm) => { + const isSelected = selectedPermissionIds.has(perm.id); + return ( +
togglePermission(perm.id)} + className={`p-3.5 rounded-[10px] border transition-all cursor-pointer select-none flex items-start gap-3 ${ + isSelected + ? 'bg-emerald-50/80 border-emerald-300 shadow-sm' + : 'bg-white border-gray-200 hover:border-gray-300 hover:bg-gray-50/50' + } ${isSystemRole ? 'opacity-80 cursor-default' : ''}`} + > +
+ togglePermission(perm.id)} + onClick={(e) => e.stopPropagation()} + /> +
+
+ + {perm.name} + + + {perm.code} + + {perm.description && ( +

+ {perm.description} +

+ )} +
+
+ ); + })} +
+
+ ); + })} +
+
- {/* Footer save buttons */} -
+ {/* ─── Sticky Footer ─────────────────────────────────────────────── */} +
+
+ Permissions Selected: + + {selectedPermissionIds.size} Active + + {isSystemRole && ( + + System Role (Read-only) + + )} +
+
Cancel {!isSystemRole && ( } + className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm disabled:opacity-50" + onClick={() => handleSubmit()} + disabled={loading || !name.trim() || selectedPermissionIds.size === 0} loading={loading} - disabled={loading} - onClick={handleSubmit} - className="!rounded-[10px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]" > - {roleToEdit ? 'Save Changes' : 'Create Role'} + {loading ? 'Saving...' : roleToEdit ? 'Save Changes' : 'Create Role'} )}
- +
+ + {/* Success Modal */} + { + setShowSuccessModal(false); + onSaved(); + }} + title={successTitle} + label="ROLE NAME" + cohortName={name} + cohortDescription={description} + />
); } diff --git a/src/app/roles/components/RolesList.tsx b/src/app/roles/components/RolesList.tsx index 3395581..be5e23d 100644 --- a/src/app/roles/components/RolesList.tsx +++ b/src/app/roles/components/RolesList.tsx @@ -1,12 +1,17 @@ -import { useEffect, useState, useMemo } from 'react'; +import { useEffect, useState, useMemo, useCallback } from 'react'; import { RolesApi, type RoleItem } from '../RolesApi'; import { + CustomTable, CustomInput, CustomButton, CustomAlertBanner, CustomConfirmationModal, + CustomActionMenu, + CustomActionItem, + CustomCheckBox, Skeleton, } from '../../../components/custom'; +import type { Column } from '../../../components/custom/CustomTable'; import { ShieldCheckIcon, PlusIcon, @@ -18,6 +23,26 @@ import { MagnifyingGlassIcon, } from '@phosphor-icons/react'; 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}} +
+ ); +} interface RolesListProps { onAddRole: () => void; @@ -28,27 +53,35 @@ 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(); - }, []); + // Pagination & Search + const [currentPage, setCurrentPage] = useState(1); + const [search, setSearch] = useState(''); - const loadRoles = async () => { + // Row selection + const [selectedIds, setSelectedIds] = useState>(new Set()); + + // Delete modal state + const [deleteConfirmRole, setDeleteConfirmRole] = useState(null); + const [deleting, setDeleting] = useState(false); + + const loadRoles = useCallback(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'); + setError(err?.response?.data?.message || 'Failed to load roles list.'); } finally { setLoading(false); } - }; + }, []); + + useEffect(() => { + loadRoles(); + }, [loadRoles]); const handleDelete = async () => { if (!deleteConfirmRole) return; @@ -65,6 +98,7 @@ export default function RolesList({ onAddRole, onEditRole }: RolesListProps) { } }; + // Filter & Pagination const filteredRoles = useMemo(() => { const q = search.toLowerCase().trim(); if (!q) return roles; @@ -76,6 +110,161 @@ export default function RolesList({ onAddRole, onEditRole }: RolesListProps) { ); }, [roles, search]); + const totalItems = filteredRoles.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 paginatedRoles = useMemo(() => { + const start = (currentPage - 1) * PAGE_SIZE; + return filteredRoles.slice(start, start + PAGE_SIZE); + }, [filteredRoles, currentPage]); + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleSearchChange = (val: string) => { + setSearch(val); + setCurrentPage(1); + }; + + // Row selection handlers + const toggleSelectAll = () => { + if (paginatedRoles.length > 0 && selectedIds.size === paginatedRoles.length) { + setSelectedIds(new Set()); + } else { + setSelectedIds(new Set(paginatedRoles.map((r) => r.id))); + } + }; + + const toggleSelectOne = (id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + // Table columns definition + const columns: Column[] = [ + { + header: ( + 0 && selectedIds.size === paginatedRoles.length} + onChange={toggleSelectAll} + /> + ), + className: 'w-[40px] pr-0', + accessor: (row) => ( + toggleSelectOne(row.id)} + onClick={(e) => e.stopPropagation()} + /> + ), + }, + { + header: , + accessor: (row) => ( +
+
+ +
+
+
+ + {row.name} + + {row.isSystem && ( + + + System Default + + )} +
+ + {row.slug} + +
+
+ ), + }, + { + header: , + accessor: (row) => ( +

+ {row.description || 'No description configured for this operational role.'} +

+ ), + }, + { + header: , + accessor: (row) => ( + + + {row.permissionCount} rules + + ), + }, + { + header: , + accessor: (row) => ( + + + {row.userCount} users + + ), + }, + { + header: , + accessor: (row) => ( + + {row.createdAt ? formatDate(row.createdAt) : '—'} + + ), + }, + { + header: , + accessor: (row) => ( +
+ + + onEditRole(row)} + icon={} + > + {row.isSystem ? 'View Permissions' : 'Edit Role'} + + + + {!row.isSystem && ( + + } + onClick={() => setDeleteConfirmRole(row)} + > + Delete Role + + + )} + +
+ ), + }, + ]; + if (loading) { return (
@@ -85,9 +274,9 @@ export default function RolesList({ onAddRole, onEditRole }: RolesListProps) {
-
- {Array.from({ length: 3 }).map((_, i) => ( - +
+ {Array.from({ length: 6 }).map((_, i) => ( + ))}
@@ -111,137 +300,44 @@ export default function RolesList({ onAddRole, onEditRole }: RolesListProps) { /> )} - {/* 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 - - - )} -
+ {/* Standardized Table Section */} + + columns={columns} + data={paginatedRoles} + 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={onAddRole} + > + Create Role + + + } + currentPage={currentPage} + totalPages={totalPages} + totalItems={totalItems} + startIndex={startIndex} + endIndex={endIndex} + onPageChange={handlePageChange} + itemName="Roles" + rowClassName={() => 'bg-white border-b border-gray-100 hover:bg-gray-50/60'} + /> {/* Delete Confirmation Modal */} = ({
- +

@@ -51,7 +51,7 @@ export const ProtectedRoute: React.FC = ({ return (

- +

Access Restricted

diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx index 1d24c5b..765e99c 100644 --- a/src/layout/AppSidebar.tsx +++ b/src/layout/AppSidebar.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { Link, useLocation } from "react-router-dom"; import { SquaresFourIcon, @@ -13,13 +13,14 @@ import { CaretDoubleRightIcon, ShieldCheckIcon, LockKeyIcon, + CaretUpIcon, } from "@phosphor-icons/react"; import { useAuth } from "../context/AuthContext"; interface NavItem { label: string; path: string; - icon: any; + icon: React.ElementType; permission?: string; } @@ -43,8 +44,34 @@ interface AppSidebarProps { export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) { const location = useLocation(); const [isCollapsed, setIsCollapsed] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const menuRef = useRef(null); const { user, logout, hasPermission } = useAuth(); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsMenuOpen(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsMenuOpen(false); + } + }; + + if (isMenuOpen) { + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isMenuOpen]); + const visibleNavItems = NAV_ITEMS.filter((item) => { if (!item.permission) return true; return hasPermission(item.permission); @@ -104,7 +131,10 @@ export default function AppSidebar({ isOpen, onClose }: AppSidebarProps) { - + + + +

+
+ + {/* User Profile Card (Clickable Trigger) */} +
setIsMenuOpen((prev) => !prev)} + title={isCollapsed ? userDisplayName : undefined} + className={`bg-gradient-to-br from-[#FAFCFB] to-[#E3EFE9] border border-white rounded-[24px] shadow-[0_4px_12px_-4px_rgba(0,0,0,0.05)] hover:shadow-md relative overflow-hidden transition-all duration-300 cursor-pointer select-none ${ + isCollapsed + ? "p-2 flex items-center justify-center h-12" + : "p-3 flex items-center justify-between" + } ${isMenuOpen ? "ring-2 ring-primary/20 shadow-md" : ""}`} + > + {/* Soft decorative glow */} + {!isCollapsed && ( +
+ )}
-
- {initials} -
- {!isCollapsed && ( -
- - {userDisplayName} - - - {roleDisplayName} - +
+
+ {initials}
+ {!isCollapsed && ( +
+ + {userDisplayName} + + + {roleDisplayName} + +
+ )} +
+ + {!isCollapsed && ( + )}