'use client'; import { useState, useCallback } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter, useParams } from 'next/navigation'; import { useTranslation } from 'react-i18next'; import { apiClient } from '@/infrastructure/api/api-client'; import { queryKeys } from '@/infrastructure/api/query-keys'; import { cn } from '@/lib/utils'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface UserDetail { id: string; displayName: string; email: string; role: 'admin' | 'operator' | 'viewer'; tenantId: string; tenantName: string; status: 'active' | 'disabled'; lastLoginAt: string | null; createdAt: string; updatedAt: string; } interface ActivityEntry { id: string; action: string; resource: string; details: string; ipAddress: string; createdAt: string; } interface ActivityResponse { data: ActivityEntry[]; total: number; } interface EditFormData { displayName: string; role: 'admin' | 'operator' | 'viewer'; status: 'active' | 'disabled'; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function formatDate(dateStr: string | null): string { if (!dateStr) return 'Never'; try { return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return dateStr; } } function formatDateTime(dateStr: string | null): string { if (!dateStr) return 'Never'; try { return new Date(dateStr).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return dateStr; } } function formatRelativeTime(dateStr: string): string { try { const diff = Date.now() - new Date(dateStr).getTime(); const seconds = Math.floor(diff / 1000); if (seconds < 0) return 'just now'; if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } catch { return dateStr; } } // --------------------------------------------------------------------------- // Role badge // --------------------------------------------------------------------------- function RoleBadge({ role }: { role: UserDetail['role'] }) { const styles: Record = { admin: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400', operator: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400', viewer: 'bg-gray-100 text-gray-800 dark:bg-gray-800/40 dark:text-gray-300', }; return ( {role} ); } // --------------------------------------------------------------------------- // Status badge // --------------------------------------------------------------------------- function StatusBadge({ status }: { status: UserDetail['status'] }) { const styles: Record = { active: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', disabled: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', }; return ( {status} ); } // --------------------------------------------------------------------------- // Activity action badge // --------------------------------------------------------------------------- function ActionBadge({ action }: { action: string }) { const map: Record = { login: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', logout: 'bg-gray-100 text-gray-800 dark:bg-gray-800/40 dark:text-gray-300', create: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400', update: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', delete: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', execute: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400', }; const style = map[action.toLowerCase()] ?? 'bg-gray-100 text-gray-800 dark:bg-gray-800/40 dark:text-gray-300'; return ( {action} ); } // --------------------------------------------------------------------------- // Info row component // --------------------------------------------------------------------------- function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label}
{value || '--'}
); } // --------------------------------------------------------------------------- // Delete confirmation dialog // --------------------------------------------------------------------------- function DeleteDialog({ open, userName, deleting, onClose, onConfirm, }: { open: boolean; userName: string; deleting: boolean; onClose: () => void; onConfirm: () => void; }) { const { t } = useTranslation('users'); const { t: tc } = useTranslation('common'); if (!open) return null; return (

{t('deleteDialog.title')}

{t('deleteDialog.message')}

{t('deleteDialog.warning')}
); } // --------------------------------------------------------------------------- // Reset password confirmation dialog // --------------------------------------------------------------------------- function ResetPasswordDialog({ open, userName, resetting, success, onClose, onConfirm, }: { open: boolean; userName: string; resetting: boolean; success: boolean; onClose: () => void; onConfirm: () => void; }) { const { t } = useTranslation('users'); const { t: tc } = useTranslation('common'); if (!open) return null; return (

{t('detail.resetPasswordDialog.title')}

{success ? ( <>
{t('detail.resetPasswordDialog.success')}
) : ( <>

{t('detail.resetPasswordDialog.message')}

)}
); } // --------------------------------------------------------------------------- // Main page component // --------------------------------------------------------------------------- export default function UserDetailPage() { const { t } = useTranslation('users'); const { t: tc } = useTranslation('common'); const router = useRouter(); const params = useParams(); const queryClient = useQueryClient(); const id = params.id as string; const ROLES: { value: UserDetail['role']; label: string }[] = [ { value: 'admin', label: t('roles.admin') }, { value: 'operator', label: t('roles.operator') }, { value: 'viewer', label: t('roles.viewer') }, ]; const STATUSES: { value: UserDetail['status']; label: string }[] = [ { value: 'active', label: t('statuses.active') }, { value: 'disabled', label: t('statuses.disabled') }, ]; // State ---------------------------------------------------------------- const [isEditing, setIsEditing] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [resetPasswordOpen, setResetPasswordOpen] = useState(false); const [resetPasswordSuccess, setResetPasswordSuccess] = useState(false); const [form, setForm] = useState({ displayName: '', role: 'viewer', status: 'active', }); const [errors, setErrors] = useState>>({}); // Queries -------------------------------------------------------------- const { data: user, isLoading, error, } = useQuery({ queryKey: queryKeys.users.detail(id), queryFn: () => apiClient(`/api/v1/auth/users/${id}`), enabled: !!id, }); const { data: activityData } = useQuery({ queryKey: queryKeys.users.activity(id), queryFn: () => apiClient(`/api/v1/auth/users/${id}/activity`), enabled: !!id, }); const activityLog = activityData?.data ?? []; // Mutations ------------------------------------------------------------ const updateMutation = useMutation({ mutationFn: (body: Record) => apiClient(`/api/v1/auth/users/${id}`, { method: 'PUT', body, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) }); queryClient.invalidateQueries({ queryKey: queryKeys.users.all }); setIsEditing(false); }, }); const deleteMutation = useMutation({ mutationFn: () => apiClient(`/api/v1/auth/users/${id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.users.all }); router.push('/users'); }, }); const resetPasswordMutation = useMutation({ mutationFn: () => apiClient(`/api/v1/auth/users/${id}/reset-password`, { method: 'POST', }), onSuccess: () => { setResetPasswordSuccess(true); }, }); // Helpers -------------------------------------------------------------- const startEditing = useCallback(() => { if (!user) return; setForm({ displayName: user.displayName, role: user.role, status: user.status, }); setErrors({}); setIsEditing(true); }, [user]); const cancelEditing = useCallback(() => { setIsEditing(false); setErrors({}); }, []); const handleChange = useCallback( (field: keyof EditFormData, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); setErrors((prev) => { const next = { ...prev }; delete next[field]; return next; }); }, [], ); const validate = useCallback((): boolean => { const next: Partial> = {}; if (!form.displayName.trim()) next.displayName = t('validation.displayNameRequired'); setErrors(next); return Object.keys(next).length === 0; }, [form, t]); const handleSave = useCallback(() => { if (!validate()) return; const body: Record = { displayName: form.displayName.trim(), role: form.role, status: form.status, }; updateMutation.mutate(body); }, [form, validate, updateMutation]); const handleResetPasswordClose = useCallback(() => { setResetPasswordOpen(false); setResetPasswordSuccess(false); }, []); // Render --------------------------------------------------------------- // Loading state if (isLoading) { return (
{t('detail.loading')}
); } // Error state if (error) { return (
{t('detail.loadError')} {(error as Error).message}
); } if (!user) return null; return (
{/* Back link */} {/* Page header */}

{user.displayName}

{/* Two-column layout */}
{/* Left column */}
{/* User Information Card */}

{t('detail.userInformation')}

{!isEditing && ( )}
{isEditing ? ( /* ---------- Edit form ---------- */
{/* displayName */}
handleChange('displayName', e.target.value)} className={cn( 'w-full px-3 py-2 rounded-md border bg-background text-sm', errors.displayName ? 'border-destructive' : 'border-input', )} placeholder="John Doe" /> {errors.displayName && (

{errors.displayName}

)}
{/* role */}
{/* status */}
{/* Update error */} {updateMutation.isError && (
{t('detail.failedToUpdate')} {(updateMutation.error as Error).message}
)} {/* Save / Cancel */}
) : ( /* ---------- Read-only info display ---------- */
} /> } />
)}
{/* Activity Log */}

{t('detail.activityLog')}

{activityLog.length === 0 ? (

{t('detail.noActivity')}

) : (
{activityLog.map((entry) => ( ))}
{t('detail.activityTable.action')} {t('detail.activityTable.resource')} {t('detail.activityTable.details')} {t('detail.activityTable.ipAddress')} {t('detail.activityTable.time')}
{entry.resource || '--'} {entry.details || '--'} {entry.ipAddress || '--'} {formatRelativeTime(entry.createdAt)}
)}
{/* Right column */}
{/* Quick Actions */}

{t('detail.quickActions')}

{/* Account Summary */}

{t('detail.accountSummary')}

{tc('status')}

{tc('role')}

{t('detail.lastLogin')}

{user.lastLoginAt ? formatDateTime(user.lastLoginAt) : tc('never')}

{t('detail.memberSince')}

{formatDate(user.createdAt)}

{user.tenantName && (

{t('table.tenant')}

{user.tenantName}

)}
{/* Delete confirmation dialog */} setDeleteOpen(false)} onConfirm={() => deleteMutation.mutate()} /> {/* Reset password dialog */} resetPasswordMutation.mutate()} />
); }