MotorSignal-App/website/components/ops/operations-users.tsx

1064 lines
47 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import {
OperationsRole,
SafeUserSummary,
SafeUserDetail,
} from './types';
interface OperationsUsersProps {
role: OperationsRole;
}
export function OperationsUsers({ role: _role }: OperationsUsersProps) {
const [users, setUsers] = useState<SafeUserSummary[]>([]);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [totalCount, setTotalCount] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Filters
const [searchQuery, setSearchQuery] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'suspended'>('all');
const [verifiedFilter, setVerifiedFilter] = useState<'all' | 'verified' | 'unverified'>('all');
// Selected User Detail Modal / Drawer
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [userDetail, setUserDetail] = useState<SafeUserDetail | null>(null);
const [isLoadingDetail, setIsLoadingDetail] = useState(false);
const [detailError, setDetailError] = useState<string | null>(null);
// Action Confirmation Modal
const [actionType, setActionType] = useState<
'suspend' | 'reinstate' | 'revoke-sessions' | 'reset-password' | null
>(null);
const [actionReason, setActionReason] = useState('');
const [isSubmittingAction, setIsSubmittingAction] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const [actionSuccessMessage, setActionSuccessMessage] = useState<string | null>(null);
// Fetch users list
const fetchUsers = useCallback(
async (cursor?: string) => {
if (cursor) {
setIsLoadingMore(true);
} else {
setIsLoading(true);
setErrorMessage(null);
}
const params = new URLSearchParams();
if (appliedSearch.trim()) params.set('q', appliedSearch.trim());
if (statusFilter !== 'all') params.set('status_filter', statusFilter);
if (verifiedFilter !== 'all') params.set('verified_filter', verifiedFilter);
if (cursor) params.set('cursor', cursor);
try {
const response = await fetch(`/api/ops/users?${params.toString()}`, {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.status === 401) {
setErrorMessage('Your Operations session has expired.');
return;
}
if (response.status === 403) {
setErrorMessage('You do not have authority to view users.');
return;
}
if (!response.ok) {
setErrorMessage('Failed to load users from platform.');
return;
}
const data = await response.json();
const items: SafeUserSummary[] = Array.isArray(data?.items) ? data.items : [];
if (cursor) {
setUsers((prev) => [...prev, ...items]);
} else {
setUsers(items);
}
setNextCursor(data?.next_cursor ?? null);
if (typeof data?.total === 'number') {
setTotalCount(data.total);
}
} catch {
setErrorMessage('Unable to connect to Operations platform.');
} finally {
setIsLoading(false);
setIsLoadingMore(false);
}
},
[appliedSearch, statusFilter, verifiedFilter]
);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
// Fetch individual user detail
const fetchUserDetail = useCallback(async (targetId: string) => {
setIsLoadingDetail(true);
setDetailError(null);
try {
const response = await fetch(`/api/ops/users/${encodeURIComponent(targetId)}`, {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.status === 401) {
setDetailError('Your Operations session has expired.');
return;
}
if (response.status === 403) {
setDetailError('You do not have authority to view this user.');
return;
}
if (response.status === 404) {
setDetailError('User profile not found.');
return;
}
if (!response.ok) {
setDetailError('Failed to load user details.');
return;
}
const data = (await response.json()) as SafeUserDetail;
setUserDetail(data);
} catch {
setDetailError('Unable to connect to Operations platform.');
} finally {
setIsLoadingDetail(false);
}
}, []);
function handleSelectUser(targetId: string) {
setSelectedUserId(targetId);
setActionSuccessMessage(null);
void fetchUserDetail(targetId);
}
function handleCloseDetail() {
setSelectedUserId(null);
setUserDetail(null);
setDetailError(null);
setActionType(null);
setActionReason('');
setActionError(null);
setActionSuccessMessage(null);
}
function handleOpenAction(
type: 'suspend' | 'reinstate' | 'revoke-sessions' | 'reset-password'
) {
setActionType(type);
setActionReason('');
setActionError(null);
}
function handleCloseActionModal() {
if (isSubmittingAction) return;
setActionType(null);
setActionReason('');
setActionError(null);
}
// Handle Action Submit
async function handleExecuteAction(e: React.FormEvent) {
e.preventDefault();
if (!selectedUserId || !actionType) return;
if (actionReason.trim().length < 3) {
setActionError('Reason must be at least 3 characters long.');
return;
}
setIsSubmittingAction(true);
setActionError(null);
try {
let endpoint = '';
let bodyData: Record<string, unknown> = {};
if (actionType === 'suspend') {
endpoint = `/api/ops/users/${encodeURIComponent(selectedUserId)}/status`;
bodyData = { is_active: false, reason: actionReason.trim() };
} else if (actionType === 'reinstate') {
endpoint = `/api/ops/users/${encodeURIComponent(selectedUserId)}/status`;
bodyData = { is_active: true, reason: actionReason.trim() };
} else if (actionType === 'revoke-sessions') {
endpoint = `/api/ops/users/${encodeURIComponent(selectedUserId)}/revoke-sessions`;
bodyData = { reason: actionReason.trim() };
} else if (actionType === 'reset-password') {
endpoint = `/api/ops/users/${encodeURIComponent(selectedUserId)}/reset-password`;
bodyData = { reason: actionReason.trim() };
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify(bodyData),
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
setActionError(errorData?.detail ?? 'Action failed to execute.');
setIsSubmittingAction(false);
return;
}
const result = await response.json();
// Update state locally
if (actionType === 'suspend' || actionType === 'reinstate') {
const newIsActive = actionType === 'reinstate';
setUserDetail((prev) =>
prev ? { ...prev, is_active: newIsActive } : null
);
setUsers((prev) =>
prev.map((u) =>
u.id === selectedUserId ? { ...u, is_active: newIsActive } : u
)
);
setActionSuccessMessage(
newIsActive
? 'Account reinstated successfully.'
: 'Account suspended and active sessions revoked.'
);
} else if (actionType === 'revoke-sessions') {
setUserDetail((prev) =>
prev
? {
...prev,
sessions: prev.sessions.map((s) => ({ ...s, is_revoked: true })),
}
: null
);
setUsers((prev) =>
prev.map((u) =>
u.id === selectedUserId ? { ...u, active_sessions_count: 0 } : u
)
);
setActionSuccessMessage('All active user sessions revoked.');
} else if (actionType === 'reset-password') {
setActionSuccessMessage(
`Password reset link dispatched. Token valid until ${
result.expires_at ? formatDate(result.expires_at) : '24 hours'
}.`
);
}
setActionType(null);
setActionReason('');
} catch {
setActionError('Network error executing operation.');
} finally {
setIsSubmittingAction(false);
}
}
function handleSearchSubmit(e: React.FormEvent) {
e.preventDefault();
setAppliedSearch(searchQuery);
}
function handleClearSearch() {
setSearchQuery('');
setAppliedSearch('');
}
function formatDate(isoString?: string | null) {
if (!isoString) return 'Never';
try {
const d = new Date(isoString);
return d.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return isoString;
}
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-5 border-b border-outline-variant/30">
<div>
<span className="text-xs font-mono text-primary-fixed-dim font-semibold">
User Management &amp; Governance
</span>
<div className="flex items-center gap-3 mt-1.5">
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight">
User Management
</h1>
{totalCount !== null && (
<span className="px-2.5 py-1 rounded-full text-xs font-mono font-semibold bg-surface-container-high text-on-surface-variant border border-outline-variant/30">
{totalCount} Total
</span>
)}
</div>
<p className="text-xs sm:text-sm text-on-surface-variant mt-1.5">
Search accounts, inspect verified credentials, audit sessions, and enforce status boundaries.
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={() => fetchUsers()}
disabled={isLoading}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-surface-container-high hover:bg-surface-bright text-primary-container border border-primary-container/40 text-xs sm:text-sm font-semibold shadow-sm transition-all focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
title="Refresh users list"
>
<span
className={`material-symbols-outlined text-[18px] ${
isLoading ? 'animate-spin' : ''
}`}
>
refresh
</span>
<span>{isLoading ? 'Checking...' : 'Refresh'}</span>
</button>
</div>
</div>
{/* Filter & Search Bar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-surface-container-low p-4 sm:p-5 rounded-2xl border border-outline-variant/30 shadow-sm">
{/* Search Input */}
<form onSubmit={handleSearchSubmit} className="flex items-center gap-2.5 flex-1 max-w-xl">
<div className="relative flex-1">
<span className="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-[20px]">
search
</span>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search user by email or account ID..."
className="w-full bg-surface-container pl-10 pr-9 py-2.5 rounded-xl border border-outline-variant/30 text-xs sm:text-sm font-mono text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
/>
{searchQuery && (
<button
type="button"
onClick={handleClearSearch}
className="absolute right-3 top-1/2 -translate-y-1/2 text-outline hover:text-on-surface"
title="Clear search"
>
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
)}
</div>
<button
type="submit"
className="px-4 py-2.5 bg-primary-container text-on-primary-container rounded-xl text-xs sm:text-sm font-bold hover:bg-primary-container/90 transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none shrink-0 cursor-pointer"
>
Search
</button>
</form>
{/* Filter Pills */}
<div className="flex flex-wrap items-center gap-3">
{/* Status Filter */}
<div className="flex items-center gap-1.5 text-xs">
<span className="text-outline font-medium">Status:</span>
<div className="inline-flex rounded-lg p-0.5 bg-surface-container border border-outline-variant/30">
{(['all', 'active', 'suspended'] as const).map((st) => (
<button
key={st}
type="button"
onClick={() => setStatusFilter(st)}
className={`px-2.5 py-1 rounded-md text-[11px] font-medium capitalize transition-all ${
statusFilter === st
? 'bg-surface-container-highest text-primary-container font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
{st}
</button>
))}
</div>
</div>
{/* Verification Filter */}
<div className="flex items-center gap-1.5 text-xs">
<span className="text-outline font-medium">Verified:</span>
<div className="inline-flex rounded-lg p-0.5 bg-surface-container border border-outline-variant/30">
{(['all', 'verified', 'unverified'] as const).map((vf) => (
<button
key={vf}
type="button"
onClick={() => setVerifiedFilter(vf)}
className={`px-2.5 py-1 rounded-md text-[11px] font-medium capitalize transition-all ${
verifiedFilter === vf
? 'bg-surface-container-highest text-primary-container font-bold shadow-sm'
: 'text-on-surface-variant hover:text-on-surface'
}`}
>
{vf}
</button>
))}
</div>
</div>
</div>
</div>
{/* Error Banner */}
{errorMessage && (
<div className="p-4 rounded-xl bg-error/10 border border-error/30 text-error flex items-center gap-3 text-xs">
<span className="material-symbols-outlined text-[18px]">error</span>
<span>{errorMessage}</span>
</div>
)}
{/* Users Content: Desktop Table & Mobile Cards */}
{isLoading ? (
<div className="p-12 text-center text-outline bg-surface-container-lowest rounded-xl border border-outline-variant/20 flex flex-col items-center justify-center gap-3">
<span className="material-symbols-outlined text-3xl animate-spin text-primary-container">
progress_activity
</span>
<span className="text-xs font-medium">Loading platform users...</span>
</div>
) : users.length === 0 ? (
<div className="p-12 text-center text-outline bg-surface-container-lowest rounded-xl border border-outline-variant/20 flex flex-col items-center justify-center gap-3">
<span className="material-symbols-outlined text-3xl text-outline-variant">
group_off
</span>
<div className="space-y-1">
<p className="text-xs font-semibold text-on-surface">No users found</p>
<p className="text-[11px] text-outline">
{appliedSearch || statusFilter !== 'all' || verifiedFilter !== 'all'
? 'No users match your search and filter criteria.'
: 'No users registered in this environment.'}
</p>
</div>
</div>
) : (
<>
{/* Desktop Table */}
<div className="hidden md:block overflow-hidden rounded-2xl border border-outline-variant/30 bg-surface-container-lowest shadow-sm">
<table className="w-full text-left text-xs sm:text-sm border-collapse">
<thead>
<tr className="border-b border-outline-variant/20 bg-surface-container-low text-xs font-mono text-outline">
<th className="py-4 px-5 sm:px-6 font-semibold">User / ID</th>
<th className="py-4 px-5 sm:px-6 font-semibold">Email Status</th>
<th className="py-4 px-5 sm:px-6 font-semibold">Account Status</th>
<th className="py-4 px-5 sm:px-6 font-semibold">App Version / Device</th>
<th className="py-4 px-5 sm:px-6 font-semibold">Last Sync</th>
<th className="py-4 px-5 sm:px-6 font-semibold">Sessions</th>
<th className="py-4 px-5 sm:px-6 font-semibold">Rides</th>
<th className="py-4 px-5 sm:px-6 font-semibold text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-outline-variant/10 text-on-surface">
{users.map((u) => (
<tr
key={u.id}
onClick={() => handleSelectUser(u.id)}
className="hover:bg-surface-container/50 cursor-pointer transition-colors group focus-within:bg-surface-container"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSelectUser(u.id);
}
}}
>
<td className="py-4 px-5 sm:px-6">
<div className="flex flex-col">
<span className="text-sm font-semibold text-on-surface group-hover:text-primary-container transition-colors">
{u.email}
</span>
<span className="font-mono text-xs text-outline truncate max-w-sm">
{u.id}
</span>
</div>
</td>
<td className="py-4 px-5 sm:px-6">
{u.email_verified ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-mono font-medium bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
<span className="material-symbols-outlined text-[14px]">check_circle</span>
<span>Verified</span>
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-mono font-medium bg-amber-500/15 text-amber-400 border border-amber-500/30">
<span className="material-symbols-outlined text-[14px]">schedule</span>
<span>Unverified</span>
</span>
)}
</td>
<td className="py-4 px-5 sm:px-6">
{u.is_active ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-mono font-semibold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
<span>Active</span>
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-xs font-mono font-semibold bg-error/15 text-error border border-error/30">
<span>Suspended</span>
</span>
)}
</td>
<td className="py-4 px-5 sm:px-6 text-xs sm:text-sm font-mono text-on-surface-variant">
{u.latest_app_version ?? (
<span className="text-outline italic">Unknown</span>
)}
</td>
<td className="py-4 px-5 sm:px-6 text-xs sm:text-sm font-mono text-outline">
{formatDate(u.last_sync_at)}
</td>
<td className="py-4 px-5 sm:px-6">
<span className="px-2.5 py-1 rounded-lg font-mono text-xs font-semibold bg-surface-container-high border border-outline-variant/30 text-on-surface">
{u.active_sessions_count} active
</span>
</td>
<td className="py-4 px-5 sm:px-6 font-mono font-medium text-xs sm:text-sm">
{u.total_rides}
</td>
<td className="py-4 px-5 sm:px-6 text-right">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleSelectUser(u.id);
}}
className="px-3.5 py-1.5 rounded-xl bg-surface-container-high hover:bg-surface-bright text-xs font-semibold text-primary-container border border-primary-container/40 transition-colors shadow-sm"
>
Manage
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile Stacked Cards */}
<div className="md:hidden space-y-3">
{users.map((u) => (
<button
key={u.id}
type="button"
onClick={() => handleSelectUser(u.id)}
className="w-full text-left p-4 rounded-xl border border-outline-variant/20 bg-surface-container-lowest hover:bg-surface-container-low transition-colors shadow-sm focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none space-y-2.5"
>
<div className="flex items-start justify-between gap-2">
<div className="space-y-0.5">
<p className="font-semibold text-xs text-on-surface">
{u.email}
</p>
<p className="font-mono text-[10px] text-outline truncate max-w-[200px]">
{u.id}
</p>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{u.is_active ? (
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono font-bold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
ACTIVE
</span>
) : (
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono font-bold bg-error/15 text-error border border-error/30">
SUSPENDED
</span>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-[11px] pt-2 border-t border-outline-variant/10">
<div>
<span className="text-outline text-[10px] block">App Version</span>
<span className="font-mono text-on-surface-variant font-medium">
{u.latest_app_version ?? 'Unknown'}
</span>
</div>
<div>
<span className="text-outline text-[10px] block">Last Sync</span>
<span className="font-mono text-on-surface-variant">
{formatDate(u.last_sync_at)}
</span>
</div>
<div>
<span className="text-outline text-[10px] block">Active Sessions</span>
<span className="font-mono text-on-surface-variant font-semibold">
{u.active_sessions_count}
</span>
</div>
<div>
<span className="text-outline text-[10px] block">Total Rides</span>
<span className="font-mono text-on-surface-variant font-semibold">
{u.total_rides}
</span>
</div>
</div>
<div className="flex items-center justify-between text-[10px] font-mono pt-1 text-primary-fixed-dim">
<span>{u.email_verified ? 'Verified Email' : 'Unverified Email'}</span>
<span>Manage Profile →</span>
</div>
</button>
))}
</div>
{/* Cursor Pagination */}
{nextCursor && (
<div className="pt-2 text-center">
<button
type="button"
onClick={() => fetchUsers(nextCursor)}
disabled={isLoadingMore}
className="px-4 py-2 rounded-lg bg-surface-container border border-outline-variant/30 text-xs font-semibold text-on-surface hover:bg-surface-container-high transition-colors focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none"
>
{isLoadingMore ? 'Loading More...' : 'Load More Users'}
</button>
</div>
)}
</>
)}
{/* User Detail Drawer / Modal */}
{selectedUserId && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="user-detail-title"
className="fixed inset-0 z-50 flex items-center justify-center sm:justify-end bg-black/60 backdrop-blur-sm transition-opacity"
onKeyDown={(e) => {
if (e.key === 'Escape' && !actionType) {
handleCloseDetail();
}
}}
>
<div className="w-full sm:max-w-2xl lg:max-w-3xl h-full bg-surface-container-lowest border-l border-outline-variant/30 shadow-2xl flex flex-col overflow-hidden animate-in slide-in-from-right duration-200">
{/* Drawer Header */}
<div className="p-4 sm:p-5 border-b border-outline-variant/20 bg-surface-container-low flex items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container text-[20px]">
account_circle
</span>
<h2
id="user-detail-title"
className="text-sm font-bold text-on-surface truncate"
>
{userDetail?.email ?? 'User Profile'}
</h2>
</div>
<p className="font-mono text-[10px] text-outline truncate">
ID: {selectedUserId}
</p>
</div>
<button
type="button"
onClick={handleCloseDetail}
className="p-1 rounded-lg text-outline hover:text-on-surface hover:bg-surface-container transition-colors"
title="Close drawer"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
{/* Success Feedback Alert */}
{actionSuccessMessage && (
<div className="mx-4 mt-4 p-3 rounded-lg bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]">check_circle</span>
<span>{actionSuccessMessage}</span>
</div>
)}
{/* Detail Error */}
{detailError && (
<div className="mx-4 mt-4 p-3 rounded-lg bg-error/10 border border-error/30 text-error text-xs flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]">error</span>
<span>{detailError}</span>
</div>
)}
{/* Drawer Body */}
<div className="flex-1 overflow-y-auto p-4 sm:p-5 space-y-5">
{isLoadingDetail ? (
<div className="p-12 text-center text-outline flex flex-col items-center justify-center gap-3">
<span className="material-symbols-outlined text-3xl animate-spin text-primary-container">
progress_activity
</span>
<span className="text-xs font-medium">Loading user details...</span>
</div>
) : userDetail ? (
<>
{/* Status Badges & Quick Facts */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2.5 p-3 rounded-xl bg-surface-container-low border border-outline-variant/20">
<div className="space-y-0.5">
<span className="text-[10px] text-outline font-medium">Account</span>
<div>
{userDetail.is_active ? (
<span className="px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
Active
</span>
) : (
<span className="px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold bg-error/15 text-error border border-error/30">
Suspended
</span>
)}
</div>
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-outline font-medium">Email</span>
<div>
{userDetail.email_verified ? (
<span className="px-1.5 py-0.5 rounded text-[10px] font-mono font-medium bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
Verified
</span>
) : (
<span className="px-1.5 py-0.5 rounded text-[10px] font-mono font-medium bg-amber-500/15 text-amber-400 border border-amber-500/30">
Unverified
</span>
)}
</div>
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-outline font-medium">Role</span>
<div className="font-mono text-xs font-semibold text-on-surface capitalize">
{userDetail.role}
</div>
</div>
<div className="space-y-0.5">
<span className="text-[10px] text-outline font-medium">Subscription</span>
<div className="font-mono text-xs font-semibold text-primary-container capitalize">
{userDetail.subscription?.plan ?? 'Free'}
</div>
</div>
</div>
{/* Operational Management Actions */}
<div className="p-3.5 rounded-xl border border-outline-variant/30 bg-surface-container-low/60 space-y-3">
<h3 className="text-xs font-semibold text-outline font-mono">
Administrative Actions
</h3>
<div className="flex flex-wrap gap-2">
{userDetail.is_active ? (
<button
type="button"
onClick={() => handleOpenAction('suspend')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-error/15 text-error border border-error/30 text-xs font-bold hover:bg-error/25 transition-colors"
>
<span className="material-symbols-outlined text-[16px]">block</span>
<span>Suspend Account</span>
</button>
) : (
<button
type="button"
onClick={() => handleOpenAction('reinstate')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 border border-emerald-500/30 text-xs font-bold hover:bg-emerald-500/25 transition-colors"
>
<span className="material-symbols-outlined text-[16px]">check_circle</span>
<span>Reinstate Account</span>
</button>
)}
<button
type="button"
onClick={() => handleOpenAction('revoke-sessions')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-amber-500/15 text-amber-400 border border-amber-500/30 text-xs font-bold hover:bg-amber-500/25 transition-colors"
>
<span className="material-symbols-outlined text-[16px]">logout</span>
<span>Revoke All Sessions</span>
</button>
<button
type="button"
onClick={() => handleOpenAction('reset-password')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-surface-container-high text-on-surface border border-outline-variant/30 text-xs font-bold hover:bg-surface-container-highest transition-colors"
>
<span className="material-symbols-outlined text-[16px]">lock_reset</span>
<span>Send Password Reset</span>
</button>
</div>
</div>
{/* Account Metadata */}
<div className="space-y-2">
<h3 className="text-xs font-semibold text-outline font-mono">
Account Timestamps
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-xs bg-surface-container-lowest p-3 rounded-lg border border-outline-variant/20">
<div>
<span className="text-[10px] text-outline block">Registered</span>
<span className="font-mono text-on-surface">
{formatDate(userDetail.created_at)}
</span>
</div>
<div>
<span className="text-[10px] text-outline block">Last Updated</span>
<span className="font-mono text-on-surface">
{formatDate(userDetail.updated_at)}
</span>
</div>
</div>
</div>
{/* Rides Summary */}
<div className="space-y-2">
<h3 className="text-xs font-semibold text-outline font-mono">
Rides Summary
</h3>
<div className="grid grid-cols-2 gap-3 p-3 rounded-lg bg-surface-container-lowest border border-outline-variant/20 text-xs">
<div>
<span className="text-[10px] text-outline block">Total Rides Recorded</span>
<span className="font-mono text-sm font-bold text-primary-container">
{userDetail.ride_summary.total_rides}
</span>
</div>
<div>
<span className="text-[10px] text-outline block">Last Ride Timestamp</span>
<span className="font-mono text-xs text-on-surface">
{formatDate(userDetail.ride_summary.last_ride_at)}
</span>
</div>
</div>
</div>
{/* Device & Session History */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-outline font-mono">
Active Sessions & Devices ({userDetail.sessions.length})
</h3>
</div>
{userDetail.sessions.length === 0 ? (
<p className="text-xs text-outline italic p-3 bg-surface-container-lowest rounded-lg border border-outline-variant/20">
No recorded sessions for this user.
</p>
) : (
<div className="space-y-2">
{userDetail.sessions.map((s) => (
<div
key={s.id}
className="p-3 rounded-lg bg-surface-container-lowest border border-outline-variant/20 flex items-center justify-between gap-3 text-xs"
>
<div className="space-y-0.5 min-w-0">
<div className="flex items-center gap-1.5">
<span className="material-symbols-outlined text-[16px] text-outline">
devices
</span>
<span className="font-semibold text-on-surface truncate">
{s.device_name ?? 'Unknown Device'}
</span>
</div>
<p className="font-mono text-[10px] text-outline">
Created: {formatDate(s.created_at)} | Last used: {formatDate(s.last_used_at)}
</p>
</div>
<div>
{s.is_revoked ? (
<span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-surface-container text-outline border border-outline-variant/20">
Revoked
</span>
) : (
<span className="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
Active
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Recent Audit Log */}
<div className="space-y-2">
<h3 className="text-xs font-semibold text-outline font-mono">
Recent Activity & Security Audit ({userDetail.recent_audit.length})
</h3>
{userDetail.recent_audit.length === 0 ? (
<p className="text-xs text-outline italic p-3 bg-surface-container-lowest rounded-lg border border-outline-variant/20">
No audit events recorded for this user.
</p>
) : (
<div className="divide-y divide-outline-variant/10 rounded-lg bg-surface-container-lowest border border-outline-variant/20 overflow-hidden">
{userDetail.recent_audit.map((evt, idx) => (
<div
key={idx}
className="p-2.5 flex items-center justify-between gap-2 text-[11px]"
>
<div className="space-y-0.5">
<span className="font-mono font-semibold text-on-surface">
{evt.action}
</span>
{evt.ip && (
<span className="text-[10px] font-mono text-outline block">
IP: {evt.ip}
</span>
)}
</div>
<span className="font-mono text-[10px] text-outline shrink-0">
{formatDate(evt.timestamp)}
</span>
</div>
))}
</div>
)}
</div>
</>
) : null}
</div>
</div>
</div>
)}
{/* Confirmation Modal with Reason Input */}
{actionType && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="confirm-action-title"
className="fixed inset-0 z-60 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"
onKeyDown={(e) => {
if (e.key === 'Escape' && !isSubmittingAction) {
handleCloseActionModal();
}
}}
>
<div className="w-full max-w-md bg-surface-container-lowest border border-outline-variant/40 rounded-2xl p-5 shadow-2xl space-y-4 animate-in zoom-in-95 duration-150">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
<span
className={`material-symbols-outlined text-[24px] ${
actionType === 'suspend'
? 'text-error'
: actionType === 'revoke-sessions'
? 'text-amber-400'
: 'text-primary-container'
}`}
>
{actionType === 'suspend'
? 'warning'
: actionType === 'revoke-sessions'
? 'logout'
: actionType === 'reset-password'
? 'lock_reset'
: 'check_circle'}
</span>
<h3
id="confirm-action-title"
className="text-sm font-bold text-on-surface font-headline-sm"
>
{actionType === 'suspend' && 'Suspend User Account'}
{actionType === 'reinstate' && 'Reinstate User Account'}
{actionType === 'revoke-sessions' && 'Revoke All User Sessions'}
{actionType === 'reset-password' && 'Dispatch Password Reset'}
</h3>
</div>
<button
type="button"
onClick={handleCloseActionModal}
disabled={isSubmittingAction}
className="text-outline hover:text-on-surface"
>
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<p className="text-xs text-on-surface-variant">
{actionType === 'suspend' &&
'Suspending this user will immediately invalidate all active sessions, block further authentication, and halt telemetry sync until reinstated.'}
{actionType === 'reinstate' &&
'Reinstating this account will allow the user to log in and resume normal application activity.'}
{actionType === 'revoke-sessions' &&
'Revoking sessions terminates all active refresh tokens and browser/mobile sessions across all devices.'}
{actionType === 'reset-password' &&
'Generating a secure reset token creates an emergency password reset link valid for 24 hours.'}
</p>
{actionError && (
<div className="p-3 rounded-lg bg-error/10 border border-error/30 text-error text-xs flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]">error</span>
<span>{actionError}</span>
</div>
)}
<form onSubmit={handleExecuteAction} className="space-y-4">
<div className="space-y-1.5">
<label
htmlFor="action-reason-input"
className="block text-xs font-semibold text-on-surface"
>
Reason for Audit Record <span className="text-error">*</span>
</label>
<textarea
id="action-reason-input"
rows={3}
value={actionReason}
onChange={(e) => setActionReason(e.target.value)}
placeholder="State the administrative justification for this security action (min 3 characters)..."
className="w-full bg-surface-container p-3 rounded-lg border border-outline-variant/30 text-xs text-on-surface placeholder:text-outline focus-visible:ring-2 focus-visible:ring-primary-container focus-visible:outline-none resize-none"
disabled={isSubmittingAction}
required
/>
<div className="flex justify-between text-[10px] text-outline font-mono">
<span>Minimum 3 characters required</span>
<span>{actionReason.trim().length} chars</span>
</div>
</div>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-outline-variant/20">
<button
type="button"
onClick={handleCloseActionModal}
disabled={isSubmittingAction}
className="px-3 py-1.5 rounded-lg bg-surface-container text-xs font-semibold text-on-surface hover:bg-surface-container-high transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmittingAction || actionReason.trim().length < 3}
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
actionType === 'suspend'
? 'bg-error text-on-error hover:bg-error/90'
: actionType === 'revoke-sessions'
? 'bg-amber-500 text-black hover:bg-amber-400'
: 'bg-primary-container text-on-primary-container hover:bg-primary-container/90'
}`}
>
{isSubmittingAction && (
<span className="material-symbols-outlined text-[14px] animate-spin">
progress_activity
</span>
)}
<span>
{actionType === 'suspend' && 'Confirm Suspension'}
{actionType === 'reinstate' && 'Confirm Reinstatement'}
{actionType === 'revoke-sessions' && 'Revoke Sessions'}
{actionType === 'reset-password' && 'Dispatch Reset'}
</span>
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}