diff --git a/apps/frontend/app/admin/audit-logs/page.tsx b/apps/frontend/app/admin/audit-logs/page.tsx new file mode 100644 index 0000000..fda7308 --- /dev/null +++ b/apps/frontend/app/admin/audit-logs/page.tsx @@ -0,0 +1,292 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { + FileText, + Filter, + ChevronLeft, + ChevronRight, + Search, + Loader2, + Calendar, + User, + Activity, + Database, +} from 'lucide-react'; +import { AdminService } from '@/services/admin'; +import { IAuditLogResponse, IAuditLogFilters } from '@/types/admin'; + +const ACTION_COLORS: Record = { + SUSPEND_USER: 'text-red-400 bg-red-500/10', + CREATE_ESCROW: 'text-emerald-400 bg-emerald-500/10', + UPDATE_ESCROW: 'text-blue-400 bg-blue-500/10', + CONSISTENCY_CHECK: 'text-purple-400 bg-purple-500/10', + LOGIN: 'text-cyan-400 bg-cyan-500/10', + ROLE_CHANGE: 'text-yellow-400 bg-yellow-500/10', +}; + +const RESOURCE_ICONS: Record = { + USER: User, + ESCROW: Database, + SYSTEM: Activity, +}; + +export default function AdminAuditLogsPage() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [filters, setFilters] = useState({}); + const [showFilters, setShowFilters] = useState(false); + + const actionTypes = ['SUSPEND_USER', 'CREATE_ESCROW', 'UPDATE_ESCROW', 'CONSISTENCY_CHECK', 'LOGIN', 'ROLE_CHANGE']; + const resourceTypes = ['USER', 'ESCROW', 'SYSTEM']; + const pageSize = 15; + + const fetchLogs = useCallback(async () => { + setLoading(true); + try { + const result = await AdminService.getAuditLogs({ + ...filters, + page, + pageSize, + }); + setData(result); + } finally { + setLoading(false); + } + }, [page, filters]); + + useEffect(() => { + fetchLogs(); + }, [fetchLogs]); + + const totalPages = data ? Math.ceil(data.total / pageSize) : 1; + + const updateFilter = (key: keyof IAuditLogFilters, value: string) => { + setFilters(prev => ({ + ...prev, + [key]: value || undefined, + })); + setPage(1); + }; + + const clearFilters = () => { + setFilters({}); + setPage(1); + }; + + const activeFilterCount = Object.values(filters).filter(Boolean).length; + + return ( +
+
+

Audit Logs

+

+ Track all administrative actions on the platform +

+
+ + {/* Filter toggle */} +
+ + {activeFilterCount > 0 && ( + + )} +
+ + {/* Filter panel */} + {showFilters && ( +
+ {/* Actor ID */} +
+ +
+ + updateFilter('actorId', e.target.value)} + className="w-full pl-9 pr-3 py-2 bg-white/[0.03] border border-white/5 rounded-lg text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors" + /> +
+
+ + {/* Action Type */} +
+ + +
+ + {/* Resource Type */} +
+ + +
+ + {/* Date range */} +
+ +
+
+ + updateFilter('from', e.target.value)} + className="w-full pl-8 pr-2 py-2 bg-white/[0.03] border border-white/5 rounded-lg text-xs text-white focus:outline-none focus:border-purple-500/50 transition-colors" + /> +
+
+ + updateFilter('to', e.target.value)} + className="w-full pl-8 pr-2 py-2 bg-white/[0.03] border border-white/5 rounded-lg text-xs text-white focus:outline-none focus:border-purple-500/50 transition-colors" + /> +
+
+
+
+ )} + + {/* Log table */} +
+ {loading ? ( +
+ +
+ ) : ( + <> +
+ + + + + + + + + + + + {data?.data.map((log) => { + const ResourceIcon = RESOURCE_ICONS[log.resourceType] || Activity; + const actionColor = ACTION_COLORS[log.actionType] || 'text-gray-400 bg-gray-500/10'; + return ( + + + + + + + + ); + })} + {data?.data.length === 0 && ( + + + + )} + +
TimestampActorActionResourceResource ID
+

+ {new Date(log.createdAt).toLocaleDateString()} +

+

+ {new Date(log.createdAt).toLocaleTimeString()} +

+
+ {log.actorId} + + + {log.actionType.replace(/_/g, ' ')} + + + + + {log.resourceType} + + + + {log.resourceId || '—'} + +
+ +

No audit logs found

+

Try adjusting your filters

+
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+

+ Page {page} of {totalPages} ({data?.total} logs) +

+
+ + +
+
+ )} + + )} +
+
+ ); +} diff --git a/apps/frontend/app/admin/escrows/page.tsx b/apps/frontend/app/admin/escrows/page.tsx new file mode 100644 index 0000000..3174407 --- /dev/null +++ b/apps/frontend/app/admin/escrows/page.tsx @@ -0,0 +1,318 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { + Shield, + Search, + Filter, + ChevronLeft, + ChevronRight, + Eye, + RefreshCw, + X, + Loader2, + AlertCircle, + CheckCircle2, + Clock, + XCircle, + AlertTriangle, +} from 'lucide-react'; +import { AdminService } from '@/services/admin'; +import { IAdminEscrow, IAdminEscrowResponse } from '@/types/admin'; + +const STATUS_CONFIG: Record = { + ACTIVE: { color: 'text-emerald-400', bg: 'bg-emerald-500/10', icon: CheckCircle2 }, + COMPLETED: { color: 'text-blue-400', bg: 'bg-blue-500/10', icon: CheckCircle2 }, + PENDING: { color: 'text-yellow-400', bg: 'bg-yellow-500/10', icon: Clock }, + CANCELLED: { color: 'text-gray-400', bg: 'bg-gray-500/10', icon: XCircle }, + DISPUTED: { color: 'text-red-400', bg: 'bg-red-500/10', icon: AlertTriangle }, +}; + +function StatusBadge({ status }: { status: string }) { + const config = STATUS_CONFIG[status] || STATUS_CONFIG.PENDING; + const Icon = config.icon; + return ( + + + {status} + + ); +} + +function EscrowDetailModal({ + escrow, + onClose, + onConsistencyCheck, +}: { + escrow: IAdminEscrow; + onClose: () => void; + onConsistencyCheck: (id: string) => void; +}) { + const [checking, setChecking] = useState(false); + const [result, setResult] = useState<{ status: string; issues: string[] } | null>(null); + + const handleCheck = async () => { + setChecking(true); + try { + const res = await AdminService.runConsistencyCheck(escrow.id); + setResult(res); + } finally { + setChecking(false); + } + }; + + return ( +
+
+
+
+

Escrow Details

+ +
+
+
+

Title

+

{escrow.title}

+
+
+
+

Amount

+

{parseFloat(escrow.amount).toLocaleString()} {escrow.asset}

+
+
+

Status

+ +
+
+

Type

+

{escrow.type}

+
+
+

Created

+

{new Date(escrow.createdAt).toLocaleDateString()}

+
+
+ + {/* Parties */} +
+

Parties

+
+ {escrow.parties.map((party) => ( +
+
+

{party.role}

+

{party.userId}

+
+ + {party.status} + +
+ ))} +
+
+ + {/* Consistency Check */} +
+ + {result && ( +
+ {result.issues.length === 0 ? ( +
+ + No issues found — escrow is consistent. +
+ ) : ( +
+
+ + Issues detected: +
+
    + {result.issues.map((issue, i) => ( +
  • {issue}
  • + ))} +
+
+ )} +
+ )} +
+
+
+
+ ); +} + +export default function AdminEscrowsPage() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState('ALL'); + const [selectedEscrow, setSelectedEscrow] = useState(null); + + const statuses = ['ALL', 'ACTIVE', 'COMPLETED', 'PENDING', 'CANCELLED', 'DISPUTED']; + + const fetchEscrows = useCallback(async () => { + setLoading(true); + try { + const result = await AdminService.getEscrows({ + status: statusFilter === 'ALL' ? undefined : statusFilter, + page, + limit: 10, + }); + setData(result); + } finally { + setLoading(false); + } + }, [page, statusFilter]); + + useEffect(() => { + fetchEscrows(); + }, [fetchEscrows]); + + return ( +
+
+

Escrow Management

+

+ Monitor and manage all platform escrows +

+
+ + {/* Filters */} +
+
+ + {statuses.map((s) => ( + + ))} +
+
+ + {/* Table */} +
+ {loading ? ( +
+ +
+ ) : ( + <> +
+ + + + + + + + + + + + + {data?.escrows.map((escrow) => ( + + + + + + + + + ))} + +
TitleAmountStatusTypeCreatedActions
+

{escrow.title}

+

{escrow.id}

+
+

{parseFloat(escrow.amount).toLocaleString()} {escrow.asset}

+
+ + + {escrow.type} + + + {new Date(escrow.createdAt).toLocaleDateString()} + + + +
+
+ + {/* Pagination */} + {data && data.pagination.pages > 1 && ( +
+

+ Page {data.pagination.page} of {data.pagination.pages} ({data.pagination.total} escrows) +

+
+ + +
+
+ )} + + )} +
+ + {/* Detail Modal */} + {selectedEscrow && ( + setSelectedEscrow(null)} + onConsistencyCheck={(id) => console.log('consistency check', id)} + /> + )} +
+ ); +} diff --git a/apps/frontend/app/admin/layout.tsx b/apps/frontend/app/admin/layout.tsx new file mode 100644 index 0000000..b269407 --- /dev/null +++ b/apps/frontend/app/admin/layout.tsx @@ -0,0 +1,226 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { usePathname, useRouter } from 'next/navigation'; +import { + BarChart3, + Users, + Shield, + FileText, + ChevronLeft, + ChevronRight, + LayoutDashboard, + LogOut, + Menu, + X, +} from 'lucide-react'; + +const ADMIN_WALLET_ADDRESSES = [ + 'GADMIN', // Placeholder: in production, fetch from backend +]; + +const navItems = [ + { href: '/admin', label: 'Overview', icon: LayoutDashboard }, + { href: '/admin/escrows', label: 'Escrows', icon: Shield }, + { href: '/admin/users', label: 'Users', icon: Users }, + { href: '/admin/audit-logs', label: 'Audit Logs', icon: FileText }, +]; + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + const [collapsed, setCollapsed] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + const [authorized, setAuthorized] = useState(true); + + useEffect(() => { + // Client-side admin role check + // In production, verify via API call or JWT token + const savedWallet = window.localStorage.getItem('vaultix_wallet'); + if (savedWallet) { + try { + const parsed = JSON.parse(savedWallet); + // Allow all connected wallets for demo — in production, check role from backend + if (parsed.publicKey) { + setAuthorized(true); + return; + } + } catch { + // ignore + } + } + // For demo, always allow access + setAuthorized(true); + }, [router]); + + if (!authorized) { + return ( +
+
+ +

Access Denied

+

+ You do not have admin privileges to access this page. +

+ + Return Home + +
+
+ ); + } + + const isActive = (href: string) => { + if (href === '/admin') return pathname === '/admin'; + return pathname.startsWith(href); + }; + + return ( +
+ {/* Mobile overlay */} + {mobileOpen && ( +
setMobileOpen(false)} + /> + )} + + {/* Sidebar */} + + + {/* Main content */} +
+ {/* Mobile header */} +
+ + + Admin Dashboard + +
+ + {/* Page content */} +
{children}
+
+
+ ); +} diff --git a/apps/frontend/app/admin/page.tsx b/apps/frontend/app/admin/page.tsx new file mode 100644 index 0000000..c435011 --- /dev/null +++ b/apps/frontend/app/admin/page.tsx @@ -0,0 +1,316 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Users, + Shield, + TrendingUp, + Activity, + ArrowUpRight, + ArrowDownRight, + BarChart3, + Wallet, + UserPlus, + CheckCircle2, + Loader2, +} from 'lucide-react'; +import { AdminService } from '@/services/admin'; +import { IPlatformStats } from '@/types/admin'; + +function StatCard({ + title, + value, + change, + changeLabel, + icon: Icon, + gradient, +}: { + title: string; + value: string | number; + change?: number; + changeLabel?: string; + icon: React.ElementType; + gradient: string; +}) { + const isPositive = change && change > 0; + return ( +
+
+
+
+
+ +
+ {change !== undefined && ( +
+ {isPositive ? ( + + ) : ( + + )} + {Math.abs(change)}% +
+ )} +
+

+ {typeof value === 'number' ? value.toLocaleString() : value} +

+

{title}

+ {changeLabel && ( +

{changeLabel}

+ )} +
+
+ ); +} + +function RoleDistribution({ roles }: { roles: Record }) { + const total = Object.values(roles).reduce((a, b) => a + b, 0); + const colors: Record = { + USER: '#8b5cf6', + ADMIN: '#3b82f6', + SUPER_ADMIN: '#06b6d4', + }; + + return ( +
+

+ + Role Distribution +

+
+ {Object.entries(roles).map(([role, count]) => { + const pct = total > 0 ? (count / total) * 100 : 0; + return ( +
+
+ + {role.replace('_', ' ').toLowerCase()} + + + {count.toLocaleString()} ({pct.toFixed(1)}%) + +
+
+
+
+
+ ); + })} +
+
+ ); +} + +function EscrowVolumeChart() { + // Simulated chart data + const months = ['Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar']; + const values = [320, 480, 560, 720, 610, 850]; + const max = Math.max(...values); + + return ( +
+

+ + Escrow Volume (last 6 months) +

+

Monthly completed escrow count

+
+ {months.map((month, i) => { + const height = (values[i] / max) * 100; + return ( +
+ + {values[i]} + +
+
+
+ {month} +
+ ); + })} +
+
+ ); +} + +function RecentActivity() { + const activities = [ + { type: 'escrow_created', desc: 'New escrow created: "Website Dev"', time: '5m ago', icon: Shield }, + { type: 'user_joined', desc: 'New user registered: GAX3...', time: '12m ago', icon: UserPlus }, + { type: 'escrow_completed', desc: 'Escrow completed: "API Integration"', time: '28m ago', icon: CheckCircle2 }, + { type: 'user_joined', desc: 'New user registered: GBK2...', time: '1h ago', icon: UserPlus }, + { type: 'escrow_created', desc: 'New escrow created: "DeFi Audit"', time: '2h ago', icon: Shield }, + ]; + + return ( +
+

+ + Recent Activity +

+
+ {activities.map((item, i) => { + const Icon = item.icon; + return ( +
+
+ +
+
+

{item.desc}

+

{item.time}

+
+
+ ); + })} +
+
+ ); +} + +export default function AdminOverviewPage() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + AdminService.getStats() + .then(setStats) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( +
+
+ +

Loading dashboard...

+
+
+ ); + } + + if (!stats) return null; + + return ( +
+ {/* Page header */} +
+

Platform Overview

+

+ Real-time platform statistics and monitoring +

+
+ + {/* Stats grid */} +
+ + + + +
+ + {/* Charts & Details row */} +
+
+ +
+ +
+ + {/* Activity */} +
+ +
+

+ + Quick Metrics +

+
+
+

+ {stats.users.active.toLocaleString()} +

+

Active Users

+
+
+

+ {stats.escrows.total.toLocaleString()} +

+

Total Escrows

+
+
+

+ {( + (stats.escrows.completed / Math.max(stats.escrows.total, 1)) * + 100 + ).toFixed(1)} + % +

+

Completion Rate

+
+
+

+ {Math.round( + stats.volume.totalCompleted / + Math.max(stats.escrows.completed, 1) + ).toLocaleString()} +

+

Avg Volume (XLM)

+
+
+
+
+
+ ); +} diff --git a/apps/frontend/app/admin/users/page.tsx b/apps/frontend/app/admin/users/page.tsx new file mode 100644 index 0000000..684edd2 --- /dev/null +++ b/apps/frontend/app/admin/users/page.tsx @@ -0,0 +1,272 @@ +'use client'; + +import React, { useState, useEffect, useCallback } from 'react'; +import { + Users, + Search, + ChevronLeft, + ChevronRight, + UserX, + UserCheck, + Loader2, + Shield, + X, + AlertTriangle, +} from 'lucide-react'; +import { AdminService } from '@/services/admin'; +import { IAdminUser, IAdminUserResponse } from '@/types/admin'; + +function RoleBadge({ role }: { role: string }) { + const config: Record = { + SUPER_ADMIN: { color: 'text-cyan-400', bg: 'bg-cyan-500/10' }, + ADMIN: { color: 'text-blue-400', bg: 'bg-blue-500/10' }, + USER: { color: 'text-gray-400', bg: 'bg-gray-500/10' }, + }; + const c = config[role] || config.USER; + return ( + + {role === 'SUPER_ADMIN' && } + {role.replace('_', ' ')} + + ); +} + +function ConfirmDialog({ + user, + onConfirm, + onCancel, + loading, +}: { + user: IAdminUser; + onConfirm: () => void; + onCancel: () => void; + loading: boolean; +}) { + const action = user.isActive ? 'Suspend' : 'Unsuspend'; + return ( +
+
+
+ +
+
+ +
+
+

{action} User

+

This action can be reversed.

+
+
+
+

Wallet Address

+

{user.walletAddress}

+

User ID

+

{user.id}

+
+
+ + +
+
+
+ ); +} + +export default function AdminUsersPage() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [search, setSearch] = useState(''); + const [confirmUser, setConfirmUser] = useState(null); + const [suspending, setSuspending] = useState(false); + + const fetchUsers = useCallback(async () => { + setLoading(true); + try { + const result = await AdminService.getUsers(page, 15, search || undefined); + setData(result); + } finally { + setLoading(false); + } + }, [page, search]); + + useEffect(() => { + const timer = setTimeout(fetchUsers, 300); + return () => clearTimeout(timer); + }, [fetchUsers]); + + const handleSuspend = async () => { + if (!confirmUser) return; + setSuspending(true); + try { + await AdminService.suspendUser(confirmUser.id); + setConfirmUser(null); + fetchUsers(); + } finally { + setSuspending(false); + } + }; + + return ( +
+
+

User Management

+

+ View and manage platform users +

+
+ + {/* Search */} +
+ + { setSearch(e.target.value); setPage(1); }} + className="w-full pl-10 pr-4 py-2.5 bg-[#12121a] border border-white/10 rounded-lg text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 transition-colors" + /> +
+ + {/* Table */} +
+ {loading ? ( +
+ +
+ ) : ( + <> +
+ + + + + + + + + + + + {data?.users.map((user) => ( + + + + + + + + ))} + +
Wallet AddressRoleStatusJoinedActions
+

+ {user.walletAddress} +

+

{user.id}

+
+ + + +
+ {user.isActive ? 'Active' : 'Suspended'} + +
+ + {new Date(user.createdAt).toLocaleDateString()} + + + {user.role !== 'SUPER_ADMIN' && ( + + )} +
+
+ + {/* Pagination */} + {data && data.pagination.pages > 1 && ( +
+

+ Page {data.pagination.page} of {data.pagination.pages} ({data.pagination.total} users) +

+
+ + +
+
+ )} + + )} +
+ + {/* Confirm dialog */} + {confirmUser && ( + setConfirmUser(null)} + loading={suspending} + /> + )} +
+ ); +} diff --git a/apps/frontend/services/admin.ts b/apps/frontend/services/admin.ts new file mode 100644 index 0000000..f785114 --- /dev/null +++ b/apps/frontend/services/admin.ts @@ -0,0 +1,155 @@ +import { + IPlatformStats, + IAdminUserResponse, + IAdminEscrowResponse, + IAuditLogResponse, + IAdminEscrowFilters, + IAuditLogFilters, + IAdminUser, + IAdminEscrow, + IAuditLog, +} from '@/types/admin'; + +// Mock data for admin dashboard + +const MOCK_STATS: IPlatformStats = { + users: { total: 1284, active: 1102, newLast30Days: 87 }, + escrows: { + total: 3452, + active: 412, + completed: 2891, + newLast30Days: 198, + completedLast30Days: 167, + }, + volume: { totalCompleted: 4285000 }, + roles: { USER: 1240, ADMIN: 38, SUPER_ADMIN: 6 }, +}; + +const MOCK_USERS: IAdminUser[] = Array.from({ length: 50 }, (_, i) => ({ + id: `user-${i + 1}`, + walletAddress: `G${String.fromCharCode(65 + (i % 26))}${'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.slice(0, 55)}`, + role: i < 2 ? 'SUPER_ADMIN' as const : i < 6 ? 'ADMIN' as const : 'USER' as const, + isActive: i !== 7 && i !== 15, + createdAt: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), +})); + +const ESCROW_STATUSES = ['ACTIVE', 'COMPLETED', 'CANCELLED', 'DISPUTED', 'PENDING']; +const ESCROW_TYPES = ['STANDARD', 'MILESTONE', 'TIMED']; + +const MOCK_ADMIN_ESCROWS: IAdminEscrow[] = Array.from({ length: 40 }, (_, i) => ({ + id: `escrow-${i + 1}`, + title: [ + 'Website Development', 'Smart Contract Audit', 'Brand Design Package', + 'API Integration', 'DeFi Protocol Development', 'Mobile App MVP', + 'Content Strategy', 'Security Review', 'Logo & Branding', 'Data Migration', + ][i % 10], + description: 'Lorem ipsum dolor sit amet', + amount: String(Math.floor(Math.random() * 50000) + 100), + asset: 'XLM', + status: ESCROW_STATUSES[i % ESCROW_STATUSES.length], + type: ESCROW_TYPES[i % ESCROW_TYPES.length], + createdAt: new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000).toISOString(), + updatedAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), + expiresAt: new Date(Date.now() + Math.random() * 60 * 24 * 60 * 60 * 1000).toISOString(), + isActive: i % 5 !== 4, + parties: [ + { id: `p-${i}-1`, userId: `user-${(i % 20) + 1}`, role: 'BUYER', status: 'ACCEPTED' }, + { id: `p-${i}-2`, userId: `user-${(i % 20) + 2}`, role: 'SELLER', status: 'ACCEPTED' }, + ], +})); + +const ACTION_TYPES = ['SUSPEND_USER', 'CREATE_ESCROW', 'UPDATE_ESCROW', 'CONSISTENCY_CHECK', 'LOGIN', 'ROLE_CHANGE']; +const RESOURCE_TYPES = ['USER', 'ESCROW', 'SYSTEM']; + +const MOCK_AUDIT_LOGS: IAuditLog[] = Array.from({ length: 80 }, (_, i) => ({ + id: `log-${i + 1}`, + actorId: `user-${(i % 6) + 1}`, + actionType: ACTION_TYPES[i % ACTION_TYPES.length], + resourceType: RESOURCE_TYPES[i % RESOURCE_TYPES.length], + resourceId: i % 3 === 0 ? null : `resource-${i}`, + metadata: { detail: `Action detail ${i + 1}` }, + createdAt: new Date(Date.now() - i * 3600 * 1000).toISOString(), +})); + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +export class AdminService { + static async getStats(): Promise { + await delay(600); + return { ...MOCK_STATS }; + } + + static async getUsers(page: number = 1, limit: number = 20, search?: string): Promise { + await delay(500); + let users = [...MOCK_USERS]; + + if (search) { + const term = search.toLowerCase(); + users = users.filter(u => + u.walletAddress.toLowerCase().includes(term) || + u.id.toLowerCase().includes(term) + ); + } + + const total = users.length; + const start = (page - 1) * limit; + const paginatedUsers = users.slice(start, start + limit); + + return { + users: paginatedUsers, + pagination: { page, limit, total, pages: Math.ceil(total / limit) }, + }; + } + + static async getEscrows(filters: IAdminEscrowFilters = {}): Promise { + await delay(500); + const { status, page = 1, limit = 20 } = filters; + let escrows = [...MOCK_ADMIN_ESCROWS]; + + if (status && status !== 'ALL') { + escrows = escrows.filter(e => e.status === status); + } + + const total = escrows.length; + const start = (page - 1) * limit; + const paginatedEscrows = escrows.slice(start, start + limit); + + return { + escrows: paginatedEscrows, + pagination: { page, limit, total, pages: Math.ceil(total / limit) }, + }; + } + + static async getAuditLogs(filters: IAuditLogFilters = {}): Promise { + await delay(500); + const { actorId, actionType, resourceType, page = 1, pageSize = 20 } = filters; + let logs = [...MOCK_AUDIT_LOGS]; + + if (actorId) logs = logs.filter(l => l.actorId === actorId); + if (actionType) logs = logs.filter(l => l.actionType === actionType); + if (resourceType) logs = logs.filter(l => l.resourceType === resourceType); + + const total = logs.length; + const start = (page - 1) * pageSize; + const paginatedLogs = logs.slice(start, start + pageSize); + + return { data: paginatedLogs, total }; + } + + static async suspendUser(userId: string): Promise<{ message: string; user: IAdminUser }> { + await delay(800); + const user = MOCK_USERS.find(u => u.id === userId); + if (!user) throw new Error('User not found'); + const updated = { ...user, isActive: !user.isActive }; + return { message: `User ${updated.isActive ? 'unsuspended' : 'suspended'} successfully`, user: updated }; + } + + static async runConsistencyCheck(escrowId: string): Promise<{ status: string; issues: string[] }> { + await delay(1200); + return { + status: 'completed', + issues: Math.random() > 0.5 ? [] : ['Minor state mismatch detected'], + }; + } +} diff --git a/apps/frontend/types/admin.ts b/apps/frontend/types/admin.ts new file mode 100644 index 0000000..4b5c37a --- /dev/null +++ b/apps/frontend/types/admin.ts @@ -0,0 +1,102 @@ +// Admin Dashboard Types + +export interface IPlatformStats { + users: { + total: number; + active: number; + newLast30Days: number; + }; + escrows: { + total: number; + active: number; + completed: number; + newLast30Days: number; + completedLast30Days: number; + }; + volume: { + totalCompleted: number; + }; + roles: Record; +} + +export interface IAdminUser { + id: string; + walletAddress: string; + role: 'USER' | 'ADMIN' | 'SUPER_ADMIN'; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export interface IAdminUserResponse { + users: IAdminUser[]; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +export interface IAdminEscrow { + id: string; + title: string; + description: string; + amount: string; + asset: string; + status: string; + type: string; + createdAt: string; + updatedAt: string; + expiresAt?: string; + isActive: boolean; + parties: { + id: string; + userId: string; + role: string; + status: string; + }[]; +} + +export interface IAdminEscrowResponse { + escrows: IAdminEscrow[]; + pagination: { + page: number; + limit: number; + total: number; + pages: number; + }; +} + +export interface IAuditLog { + id: string; + actorId: string; + actionType: string; + resourceType: string; + resourceId: string | null; + metadata?: Record; + createdAt: string; +} + +export interface IAuditLogResponse { + data: IAuditLog[]; + total: number; +} + +export interface IAdminEscrowFilters { + status?: string; + page?: number; + limit?: number; + startDate?: string; + endDate?: string; +} + +export interface IAuditLogFilters { + actorId?: string; + actionType?: string; + resourceType?: string; + from?: string; + to?: string; + page?: number; + pageSize?: number; +}