mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			981 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			981 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 | 
						|
import {
 | 
						|
	Calendar,
 | 
						|
	CheckCircle,
 | 
						|
	Edit,
 | 
						|
	Key,
 | 
						|
	Mail,
 | 
						|
	Shield,
 | 
						|
	Trash2,
 | 
						|
	User,
 | 
						|
	XCircle,
 | 
						|
} from "lucide-react";
 | 
						|
import { useEffect, useId, useState } from "react";
 | 
						|
import { useAuth } from "../../contexts/AuthContext";
 | 
						|
import { adminUsersAPI, permissionsAPI } from "../../utils/api";
 | 
						|
 | 
						|
const UsersTab = () => {
 | 
						|
	const [showAddModal, setShowAddModal] = useState(false);
 | 
						|
	const [editingUser, setEditingUser] = useState(null);
 | 
						|
	const [resetPasswordUser, setResetPasswordUser] = useState(null);
 | 
						|
	const queryClient = useQueryClient();
 | 
						|
	const { user: currentUser } = useAuth();
 | 
						|
 | 
						|
	// Listen for the header button event to open add modal
 | 
						|
	useEffect(() => {
 | 
						|
		const handleOpenAddModal = () => setShowAddModal(true);
 | 
						|
		window.addEventListener("openAddUserModal", handleOpenAddModal);
 | 
						|
		return () =>
 | 
						|
			window.removeEventListener("openAddUserModal", handleOpenAddModal);
 | 
						|
	}, []);
 | 
						|
 | 
						|
	// Fetch users
 | 
						|
	const {
 | 
						|
		data: users,
 | 
						|
		isLoading,
 | 
						|
		error,
 | 
						|
	} = useQuery({
 | 
						|
		queryKey: ["users"],
 | 
						|
		queryFn: () => adminUsersAPI.list().then((res) => res.data),
 | 
						|
	});
 | 
						|
 | 
						|
	// Fetch available roles
 | 
						|
	const { data: roles } = useQuery({
 | 
						|
		queryKey: ["rolePermissions"],
 | 
						|
		queryFn: () => permissionsAPI.getRoles().then((res) => res.data),
 | 
						|
	});
 | 
						|
 | 
						|
	// Delete user mutation
 | 
						|
	const deleteUserMutation = useMutation({
 | 
						|
		mutationFn: adminUsersAPI.delete,
 | 
						|
		onSuccess: () => {
 | 
						|
			queryClient.invalidateQueries(["users"]);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	// Update user mutation
 | 
						|
	const updateUserMutation = useMutation({
 | 
						|
		mutationFn: ({ id, data }) => adminUsersAPI.update(id, data),
 | 
						|
		onSuccess: () => {
 | 
						|
			queryClient.invalidateQueries(["users"]);
 | 
						|
			setEditingUser(null);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	// Reset password mutation
 | 
						|
	const resetPasswordMutation = useMutation({
 | 
						|
		mutationFn: ({ userId, newPassword }) =>
 | 
						|
			adminUsersAPI.resetPassword(userId, newPassword),
 | 
						|
		onSuccess: () => {
 | 
						|
			queryClient.invalidateQueries(["users"]);
 | 
						|
			setResetPasswordUser(null);
 | 
						|
		},
 | 
						|
	});
 | 
						|
 | 
						|
	const handleDeleteUser = async (userId, username) => {
 | 
						|
		if (
 | 
						|
			window.confirm(
 | 
						|
				`Are you sure you want to delete user "${username}"? This action cannot be undone.`,
 | 
						|
			)
 | 
						|
		) {
 | 
						|
			try {
 | 
						|
				await deleteUserMutation.mutateAsync(userId);
 | 
						|
			} catch (error) {
 | 
						|
				console.error("Failed to delete user:", error);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleUserCreated = () => {
 | 
						|
		queryClient.invalidateQueries(["users"]);
 | 
						|
		setShowAddModal(false);
 | 
						|
	};
 | 
						|
 | 
						|
	const handleEditUser = (user) => {
 | 
						|
		// Reset editingUser first to force re-render with fresh data
 | 
						|
		setEditingUser(null);
 | 
						|
		// Use setTimeout to ensure the modal re-initializes with fresh data
 | 
						|
		setTimeout(() => {
 | 
						|
			setEditingUser(user);
 | 
						|
		}, 0);
 | 
						|
	};
 | 
						|
 | 
						|
	const handleResetPassword = (user) => {
 | 
						|
		setResetPasswordUser(user);
 | 
						|
	};
 | 
						|
 | 
						|
	if (isLoading) {
 | 
						|
		return (
 | 
						|
			<div className="flex items-center justify-center h-64">
 | 
						|
				<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
 | 
						|
			</div>
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	if (error) {
 | 
						|
		return (
 | 
						|
			<div className="bg-danger-50 border border-danger-200 rounded-md p-4">
 | 
						|
				<div className="flex">
 | 
						|
					<XCircle className="h-5 w-5 text-danger-400" />
 | 
						|
					<div className="ml-3">
 | 
						|
						<h3 className="text-sm font-medium text-danger-800">
 | 
						|
							Error loading users
 | 
						|
						</h3>
 | 
						|
						<p className="mt-1 text-sm text-danger-700">{error.message}</p>
 | 
						|
					</div>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		);
 | 
						|
	}
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="space-y-6">
 | 
						|
			{/* Users Table */}
 | 
						|
			<div className="bg-white dark:bg-secondary-800 shadow overflow-hidden sm:rounded-lg">
 | 
						|
				<div className="overflow-x-auto">
 | 
						|
					<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600">
 | 
						|
						<thead className="bg-secondary-50 dark:bg-secondary-700">
 | 
						|
							<tr>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									User
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Email
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Role
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Status
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Created
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-left text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Last Login
 | 
						|
								</th>
 | 
						|
								<th className="px-6 py-3 text-right text-xs font-medium text-secondary-500 dark:text-secondary-300 uppercase tracking-wider">
 | 
						|
									Actions
 | 
						|
								</th>
 | 
						|
							</tr>
 | 
						|
						</thead>
 | 
						|
						<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
 | 
						|
							{users && Array.isArray(users) && users.length > 0 ? (
 | 
						|
								users.map((user) => (
 | 
						|
									<tr
 | 
						|
										key={user.id}
 | 
						|
										className="hover:bg-secondary-50 dark:hover:bg-secondary-700"
 | 
						|
									>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap">
 | 
						|
											<div className="flex items-center">
 | 
						|
												<div className="flex-shrink-0 h-10 w-10">
 | 
						|
													<div className="h-10 w-10 rounded-full bg-primary-100 flex items-center justify-center">
 | 
						|
														<User className="h-5 w-5 text-primary-600" />
 | 
						|
													</div>
 | 
						|
												</div>
 | 
						|
												<div className="ml-4">
 | 
						|
													<div className="flex items-center">
 | 
						|
														<div className="text-sm font-medium text-secondary-900 dark:text-white">
 | 
						|
															{user.username}
 | 
						|
														</div>
 | 
						|
														{user.id === currentUser?.id && (
 | 
						|
															<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
 | 
						|
																You
 | 
						|
															</span>
 | 
						|
														)}
 | 
						|
													</div>
 | 
						|
												</div>
 | 
						|
											</div>
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap">
 | 
						|
											<div className="flex items-center text-sm text-secondary-500 dark:text-secondary-300">
 | 
						|
												<Mail className="h-4 w-4 mr-2" />
 | 
						|
												{user.email}
 | 
						|
											</div>
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap">
 | 
						|
											<span
 | 
						|
												className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
 | 
						|
													user.role === "admin"
 | 
						|
														? "bg-primary-100 text-primary-800"
 | 
						|
														: user.role === "host_manager"
 | 
						|
															? "bg-green-100 text-green-800"
 | 
						|
															: user.role === "readonly"
 | 
						|
																? "bg-yellow-100 text-yellow-800"
 | 
						|
																: "bg-secondary-100 text-secondary-800"
 | 
						|
												}`}
 | 
						|
											>
 | 
						|
												<Shield className="h-3 w-3 mr-1" />
 | 
						|
												{user.role.charAt(0).toUpperCase() +
 | 
						|
													user.role.slice(1).replace("_", " ")}
 | 
						|
											</span>
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap">
 | 
						|
											{user.is_active ? (
 | 
						|
												<div className="flex items-center text-green-600">
 | 
						|
													<CheckCircle className="h-4 w-4 mr-1" />
 | 
						|
													<span className="text-sm">Active</span>
 | 
						|
												</div>
 | 
						|
											) : (
 | 
						|
												<div className="flex items-center text-red-600">
 | 
						|
													<XCircle className="h-4 w-4 mr-1" />
 | 
						|
													<span className="text-sm">Inactive</span>
 | 
						|
												</div>
 | 
						|
											)}
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap">
 | 
						|
											<div className="flex items-center text-sm text-secondary-500 dark:text-secondary-300">
 | 
						|
												<Calendar className="h-4 w-4 mr-2" />
 | 
						|
												{new Date(user.created_at).toLocaleDateString()}
 | 
						|
											</div>
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap text-sm text-secondary-500 dark:text-secondary-300">
 | 
						|
											{user.last_login ? (
 | 
						|
												new Date(user.last_login).toLocaleDateString()
 | 
						|
											) : (
 | 
						|
												<span className="text-secondary-400">Never</span>
 | 
						|
											)}
 | 
						|
										</td>
 | 
						|
										<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
 | 
						|
											<div className="flex items-center justify-end space-x-2">
 | 
						|
												<button
 | 
						|
													type="button"
 | 
						|
													onClick={() => handleEditUser(user)}
 | 
						|
													className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
 | 
						|
													title="Edit user"
 | 
						|
												>
 | 
						|
													<Edit className="h-4 w-4" />
 | 
						|
												</button>
 | 
						|
												<button
 | 
						|
													type="button"
 | 
						|
													onClick={() => handleResetPassword(user)}
 | 
						|
													className="text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-300 disabled:text-gray-300 disabled:cursor-not-allowed"
 | 
						|
													title={
 | 
						|
														!user.is_active
 | 
						|
															? "Cannot reset password for inactive user"
 | 
						|
															: "Reset password"
 | 
						|
													}
 | 
						|
													disabled={!user.is_active}
 | 
						|
												>
 | 
						|
													<Key className="h-4 w-4" />
 | 
						|
												</button>
 | 
						|
												<button
 | 
						|
													type="button"
 | 
						|
													onClick={() =>
 | 
						|
														handleDeleteUser(user.id, user.username)
 | 
						|
													}
 | 
						|
													className="text-danger-400 hover:text-danger-600 dark:text-danger-500 dark:hover:text-danger-400 disabled:text-gray-300 disabled:cursor-not-allowed"
 | 
						|
													title={
 | 
						|
														user.id === currentUser?.id
 | 
						|
															? "Cannot delete your own account"
 | 
						|
															: user.role === "admin" &&
 | 
						|
																	users.filter((u) => u.role === "admin")
 | 
						|
																		.length === 1
 | 
						|
																? "Cannot delete the last admin user"
 | 
						|
																: "Delete user"
 | 
						|
													}
 | 
						|
													disabled={
 | 
						|
														user.id === currentUser?.id ||
 | 
						|
														(user.role === "admin" &&
 | 
						|
															users.filter((u) => u.role === "admin").length ===
 | 
						|
																1)
 | 
						|
													}
 | 
						|
												>
 | 
						|
													<Trash2 className="h-4 w-4" />
 | 
						|
												</button>
 | 
						|
											</div>
 | 
						|
										</td>
 | 
						|
									</tr>
 | 
						|
								))
 | 
						|
							) : (
 | 
						|
								<tr>
 | 
						|
									<td colSpan="7" className="px-6 py-12 text-center">
 | 
						|
										<User className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
 | 
						|
										<p className="text-secondary-500 dark:text-secondary-300">
 | 
						|
											No users found
 | 
						|
										</p>
 | 
						|
										<p className="text-sm text-secondary-400 dark:text-secondary-400 mt-2">
 | 
						|
											Click "Add User" to create the first user
 | 
						|
										</p>
 | 
						|
									</td>
 | 
						|
								</tr>
 | 
						|
							)}
 | 
						|
						</tbody>
 | 
						|
					</table>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
 | 
						|
			{/* Add User Modal */}
 | 
						|
			<AddUserModal
 | 
						|
				isOpen={showAddModal}
 | 
						|
				onClose={() => setShowAddModal(false)}
 | 
						|
				onUserCreated={handleUserCreated}
 | 
						|
				roles={roles}
 | 
						|
			/>
 | 
						|
 | 
						|
			{/* Edit User Modal */}
 | 
						|
			{editingUser && (
 | 
						|
				<EditUserModal
 | 
						|
					user={editingUser}
 | 
						|
					isOpen={!!editingUser}
 | 
						|
					onClose={() => setEditingUser(null)}
 | 
						|
					onUpdateUser={updateUserMutation.mutate}
 | 
						|
					isLoading={updateUserMutation.isPending}
 | 
						|
					roles={roles}
 | 
						|
				/>
 | 
						|
			)}
 | 
						|
 | 
						|
			{/* Reset Password Modal */}
 | 
						|
			{resetPasswordUser && (
 | 
						|
				<ResetPasswordModal
 | 
						|
					user={resetPasswordUser}
 | 
						|
					isOpen={!!resetPasswordUser}
 | 
						|
					onClose={() => setResetPasswordUser(null)}
 | 
						|
					onPasswordReset={resetPasswordMutation.mutate}
 | 
						|
					isLoading={resetPasswordMutation.isPending}
 | 
						|
				/>
 | 
						|
			)}
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Add User Modal Component
 | 
						|
const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => {
 | 
						|
	const usernameId = useId();
 | 
						|
	const emailId = useId();
 | 
						|
	const firstNameId = useId();
 | 
						|
	const lastNameId = useId();
 | 
						|
	const passwordId = useId();
 | 
						|
	const roleId = useId();
 | 
						|
 | 
						|
	const [formData, setFormData] = useState({
 | 
						|
		username: "",
 | 
						|
		email: "",
 | 
						|
		password: "",
 | 
						|
		first_name: "",
 | 
						|
		last_name: "",
 | 
						|
		role: "user",
 | 
						|
	});
 | 
						|
	const [isLoading, setIsLoading] = useState(false);
 | 
						|
	const [error, setError] = useState("");
 | 
						|
	const [success, setSuccess] = useState(false);
 | 
						|
 | 
						|
	// Reset form when modal is closed
 | 
						|
	useEffect(() => {
 | 
						|
		if (!isOpen) {
 | 
						|
			setFormData({
 | 
						|
				username: "",
 | 
						|
				email: "",
 | 
						|
				password: "",
 | 
						|
				first_name: "",
 | 
						|
				last_name: "",
 | 
						|
				role: "user",
 | 
						|
			});
 | 
						|
			setError("");
 | 
						|
			setSuccess(false);
 | 
						|
		}
 | 
						|
	}, [isOpen]);
 | 
						|
 | 
						|
	const handleSubmit = async (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		setIsLoading(true);
 | 
						|
		setError("");
 | 
						|
		setSuccess(false);
 | 
						|
 | 
						|
		try {
 | 
						|
			// Only send role if roles are available from API
 | 
						|
			const payload = {
 | 
						|
				username: formData.username,
 | 
						|
				email: formData.email,
 | 
						|
				password: formData.password,
 | 
						|
				first_name: formData.first_name,
 | 
						|
				last_name: formData.last_name,
 | 
						|
			};
 | 
						|
			if (roles && Array.isArray(roles) && roles.length > 0) {
 | 
						|
				payload.role = formData.role;
 | 
						|
			}
 | 
						|
			await adminUsersAPI.create(payload);
 | 
						|
			setSuccess(true);
 | 
						|
			onUserCreated();
 | 
						|
			// Auto-close after 1.5 seconds
 | 
						|
			setTimeout(() => {
 | 
						|
				onClose();
 | 
						|
			}, 1500);
 | 
						|
		} catch (err) {
 | 
						|
			setError(err.response?.data?.error || "Failed to create user");
 | 
						|
		} finally {
 | 
						|
			setIsLoading(false);
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleInputChange = (e) => {
 | 
						|
		setFormData({
 | 
						|
			...formData,
 | 
						|
			[e.target.name]: e.target.value,
 | 
						|
		});
 | 
						|
	};
 | 
						|
 | 
						|
	if (!isOpen) return null;
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
 | 
						|
				<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
 | 
						|
					Add New User
 | 
						|
				</h3>
 | 
						|
 | 
						|
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={usernameId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Username
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={usernameId}
 | 
						|
							type="text"
 | 
						|
							name="username"
 | 
						|
							required
 | 
						|
							value={formData.username}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={emailId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Email
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={emailId}
 | 
						|
							type="email"
 | 
						|
							name="email"
 | 
						|
							required
 | 
						|
							value={formData.email}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="grid grid-cols-2 gap-4">
 | 
						|
						<div>
 | 
						|
							<label
 | 
						|
								htmlFor={firstNameId}
 | 
						|
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
							>
 | 
						|
								First Name
 | 
						|
							</label>
 | 
						|
							<input
 | 
						|
								id={firstNameId}
 | 
						|
								type="text"
 | 
						|
								name="first_name"
 | 
						|
								value={formData.first_name}
 | 
						|
								onChange={handleInputChange}
 | 
						|
								className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							/>
 | 
						|
						</div>
 | 
						|
						<div>
 | 
						|
							<label
 | 
						|
								htmlFor={lastNameId}
 | 
						|
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
							>
 | 
						|
								Last Name
 | 
						|
							</label>
 | 
						|
							<input
 | 
						|
								id={lastNameId}
 | 
						|
								type="text"
 | 
						|
								name="last_name"
 | 
						|
								value={formData.last_name}
 | 
						|
								onChange={handleInputChange}
 | 
						|
								className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							/>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={passwordId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Password
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={passwordId}
 | 
						|
							type="password"
 | 
						|
							name="password"
 | 
						|
							required
 | 
						|
							minLength={6}
 | 
						|
							value={formData.password}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						/>
 | 
						|
						<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
 | 
						|
							Minimum 6 characters
 | 
						|
						</p>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={roleId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Role
 | 
						|
						</label>
 | 
						|
						<select
 | 
						|
							id={roleId}
 | 
						|
							name="role"
 | 
						|
							value={formData.role}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						>
 | 
						|
							{roles && Array.isArray(roles) && roles.length > 0 ? (
 | 
						|
								roles.map((role) => (
 | 
						|
									<option key={role.role} value={role.role}>
 | 
						|
										{role.role.charAt(0).toUpperCase() +
 | 
						|
											role.role.slice(1).replace("_", " ")}
 | 
						|
									</option>
 | 
						|
								))
 | 
						|
							) : (
 | 
						|
								<>
 | 
						|
									<option value="user">User</option>
 | 
						|
									<option value="admin">Admin</option>
 | 
						|
								</>
 | 
						|
							)}
 | 
						|
						</select>
 | 
						|
					</div>
 | 
						|
 | 
						|
					{success && (
 | 
						|
						<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-3">
 | 
						|
							<div className="flex items-center">
 | 
						|
								<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
 | 
						|
								<p className="text-sm text-green-700 dark:text-green-300">
 | 
						|
									User created successfully!
 | 
						|
								</p>
 | 
						|
							</div>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					{error && (
 | 
						|
						<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
 | 
						|
							<p className="text-sm text-danger-700 dark:text-danger-300">
 | 
						|
								{error}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					<div className="flex justify-end space-x-3">
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 border border-secondary-300 dark:border-secondary-600 rounded-md hover:bg-secondary-50 dark:hover:bg-secondary-600"
 | 
						|
						>
 | 
						|
							Cancel
 | 
						|
						</button>
 | 
						|
						<button
 | 
						|
							type="submit"
 | 
						|
							disabled={isLoading}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md hover:bg-primary-700 disabled:opacity-50"
 | 
						|
						>
 | 
						|
							{isLoading ? "Creating..." : "Create User"}
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</form>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Edit User Modal Component
 | 
						|
const EditUserModal = ({
 | 
						|
	user,
 | 
						|
	isOpen,
 | 
						|
	onClose,
 | 
						|
	onUpdateUser,
 | 
						|
	isLoading,
 | 
						|
	roles,
 | 
						|
}) => {
 | 
						|
	const editUsernameId = useId();
 | 
						|
	const editEmailId = useId();
 | 
						|
	const editFirstNameId = useId();
 | 
						|
	const editLastNameId = useId();
 | 
						|
	const editRoleId = useId();
 | 
						|
	const editActiveId = useId();
 | 
						|
 | 
						|
	const [formData, setFormData] = useState({
 | 
						|
		username: user?.username || "",
 | 
						|
		email: user?.email || "",
 | 
						|
		first_name: user?.first_name || "",
 | 
						|
		last_name: user?.last_name || "",
 | 
						|
		role: user?.role || "user",
 | 
						|
		is_active: user?.is_active ?? true,
 | 
						|
	});
 | 
						|
	const [error, setError] = useState("");
 | 
						|
	const [success, setSuccess] = useState(false);
 | 
						|
 | 
						|
	// Update formData when user prop changes or modal opens
 | 
						|
	useEffect(() => {
 | 
						|
		if (user && isOpen) {
 | 
						|
			setFormData({
 | 
						|
				username: user.username || "",
 | 
						|
				email: user.email || "",
 | 
						|
				first_name: user.first_name || "",
 | 
						|
				last_name: user.last_name || "",
 | 
						|
				role: user.role || "user",
 | 
						|
				is_active: user.is_active ?? true,
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}, [user, isOpen]);
 | 
						|
 | 
						|
	// Reset error and success when modal closes
 | 
						|
	useEffect(() => {
 | 
						|
		if (!isOpen) {
 | 
						|
			setError("");
 | 
						|
			setSuccess(false);
 | 
						|
		}
 | 
						|
	}, [isOpen]);
 | 
						|
 | 
						|
	const handleSubmit = async (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		setError("");
 | 
						|
		setSuccess(false);
 | 
						|
 | 
						|
		try {
 | 
						|
			await onUpdateUser({ id: user.id, data: formData });
 | 
						|
			setSuccess(true);
 | 
						|
			// Auto-close after 1.5 seconds
 | 
						|
			setTimeout(() => {
 | 
						|
				onClose();
 | 
						|
			}, 1500);
 | 
						|
		} catch (err) {
 | 
						|
			setError(err.response?.data?.error || "Failed to update user");
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleInputChange = (e) => {
 | 
						|
		const { name, value, type, checked } = e.target;
 | 
						|
		setFormData({
 | 
						|
			...formData,
 | 
						|
			[name]: type === "checkbox" ? checked : value,
 | 
						|
		});
 | 
						|
	};
 | 
						|
 | 
						|
	if (!isOpen || !user) return null;
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
 | 
						|
				<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
 | 
						|
					Edit User
 | 
						|
				</h3>
 | 
						|
 | 
						|
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={editUsernameId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Username
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={editUsernameId}
 | 
						|
							type="text"
 | 
						|
							name="username"
 | 
						|
							required
 | 
						|
							value={formData.username}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={editEmailId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Email
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={editEmailId}
 | 
						|
							type="email"
 | 
						|
							name="email"
 | 
						|
							required
 | 
						|
							value={formData.email}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="grid grid-cols-2 gap-4">
 | 
						|
						<div>
 | 
						|
							<label
 | 
						|
								htmlFor={editFirstNameId}
 | 
						|
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
							>
 | 
						|
								First Name
 | 
						|
							</label>
 | 
						|
							<input
 | 
						|
								id={editFirstNameId}
 | 
						|
								type="text"
 | 
						|
								name="first_name"
 | 
						|
								value={formData.first_name}
 | 
						|
								onChange={handleInputChange}
 | 
						|
								className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							/>
 | 
						|
						</div>
 | 
						|
						<div>
 | 
						|
							<label
 | 
						|
								htmlFor={editLastNameId}
 | 
						|
								className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
							>
 | 
						|
								Last Name
 | 
						|
							</label>
 | 
						|
							<input
 | 
						|
								id={editLastNameId}
 | 
						|
								type="text"
 | 
						|
								name="last_name"
 | 
						|
								value={formData.last_name}
 | 
						|
								onChange={handleInputChange}
 | 
						|
								className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							/>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={editRoleId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Role
 | 
						|
						</label>
 | 
						|
						<select
 | 
						|
							id={editRoleId}
 | 
						|
							name="role"
 | 
						|
							value={formData.role}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
						>
 | 
						|
							{roles && Array.isArray(roles) ? (
 | 
						|
								roles.map((role) => (
 | 
						|
									<option key={role.role} value={role.role}>
 | 
						|
										{role.role.charAt(0).toUpperCase() +
 | 
						|
											role.role.slice(1).replace("_", " ")}
 | 
						|
									</option>
 | 
						|
								))
 | 
						|
							) : (
 | 
						|
								<>
 | 
						|
									<option value="user">User</option>
 | 
						|
									<option value="admin">Admin</option>
 | 
						|
								</>
 | 
						|
							)}
 | 
						|
						</select>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="flex items-center">
 | 
						|
						<input
 | 
						|
							id={editActiveId}
 | 
						|
							type="checkbox"
 | 
						|
							name="is_active"
 | 
						|
							checked={formData.is_active}
 | 
						|
							onChange={handleInputChange}
 | 
						|
							className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
 | 
						|
						/>
 | 
						|
						<label
 | 
						|
							htmlFor={editActiveId}
 | 
						|
							className="ml-2 block text-sm text-secondary-700 dark:text-secondary-200"
 | 
						|
						>
 | 
						|
							Active user
 | 
						|
						</label>
 | 
						|
					</div>
 | 
						|
 | 
						|
					{success && (
 | 
						|
						<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-3">
 | 
						|
							<div className="flex items-center">
 | 
						|
								<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
 | 
						|
								<p className="text-sm text-green-700 dark:text-green-300">
 | 
						|
									User updated successfully!
 | 
						|
								</p>
 | 
						|
							</div>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					{error && (
 | 
						|
						<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
 | 
						|
							<p className="text-sm text-danger-700 dark:text-danger-300">
 | 
						|
								{error}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					<div className="flex justify-end space-x-3">
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={onClose}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 border border-secondary-300 dark:border-secondary-600 rounded-md hover:bg-secondary-50 dark:hover:bg-secondary-600"
 | 
						|
						>
 | 
						|
							Cancel
 | 
						|
						</button>
 | 
						|
						<button
 | 
						|
							type="submit"
 | 
						|
							disabled={isLoading}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md hover:bg-primary-700 disabled:opacity-50"
 | 
						|
						>
 | 
						|
							{isLoading ? "Updating..." : "Update User"}
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</form>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
// Reset Password Modal Component
 | 
						|
const ResetPasswordModal = ({
 | 
						|
	user,
 | 
						|
	isOpen,
 | 
						|
	onClose,
 | 
						|
	onPasswordReset,
 | 
						|
	isLoading,
 | 
						|
}) => {
 | 
						|
	const newPasswordId = useId();
 | 
						|
	const confirmPasswordId = useId();
 | 
						|
	const [newPassword, setNewPassword] = useState("");
 | 
						|
	const [confirmPassword, setConfirmPassword] = useState("");
 | 
						|
	const [error, setError] = useState("");
 | 
						|
 | 
						|
	const handleSubmit = async (e) => {
 | 
						|
		e.preventDefault();
 | 
						|
		setError("");
 | 
						|
 | 
						|
		// Validate passwords
 | 
						|
		if (newPassword.length < 6) {
 | 
						|
			setError("Password must be at least 6 characters long");
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		if (newPassword !== confirmPassword) {
 | 
						|
			setError("Passwords do not match");
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		try {
 | 
						|
			await onPasswordReset({ userId: user.id, newPassword });
 | 
						|
			// Reset form on success
 | 
						|
			setNewPassword("");
 | 
						|
			setConfirmPassword("");
 | 
						|
		} catch (err) {
 | 
						|
			setError(err.response?.data?.error || "Failed to reset password");
 | 
						|
		}
 | 
						|
	};
 | 
						|
 | 
						|
	const handleClose = () => {
 | 
						|
		setNewPassword("");
 | 
						|
		setConfirmPassword("");
 | 
						|
		setError("");
 | 
						|
		onClose();
 | 
						|
	};
 | 
						|
 | 
						|
	if (!isOpen) return null;
 | 
						|
 | 
						|
	return (
 | 
						|
		<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
 | 
						|
			<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
 | 
						|
				<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
 | 
						|
					Reset Password for {user.username}
 | 
						|
				</h3>
 | 
						|
 | 
						|
				<form onSubmit={handleSubmit} className="space-y-4">
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={newPasswordId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							New Password
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={newPasswordId}
 | 
						|
							type="password"
 | 
						|
							required
 | 
						|
							minLength={6}
 | 
						|
							value={newPassword}
 | 
						|
							onChange={(e) => setNewPassword(e.target.value)}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							placeholder="Enter new password (min 6 characters)"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div>
 | 
						|
						<label
 | 
						|
							htmlFor={confirmPasswordId}
 | 
						|
							className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-1"
 | 
						|
						>
 | 
						|
							Confirm Password
 | 
						|
						</label>
 | 
						|
						<input
 | 
						|
							id={confirmPasswordId}
 | 
						|
							type="password"
 | 
						|
							required
 | 
						|
							value={confirmPassword}
 | 
						|
							onChange={(e) => setConfirmPassword(e.target.value)}
 | 
						|
							className="block w-full border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
 | 
						|
							placeholder="Confirm new password"
 | 
						|
						/>
 | 
						|
					</div>
 | 
						|
 | 
						|
					<div className="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-3">
 | 
						|
						<div className="flex">
 | 
						|
							<div className="flex-shrink-0">
 | 
						|
								<Key className="h-5 w-5 text-yellow-400" />
 | 
						|
							</div>
 | 
						|
							<div className="ml-3">
 | 
						|
								<h3 className="text-sm font-medium text-yellow-800 dark:text-yellow-200">
 | 
						|
									Password Reset Warning
 | 
						|
								</h3>
 | 
						|
								<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
 | 
						|
									<p>
 | 
						|
										This will immediately change the user's password. The user
 | 
						|
										will need to use the new password to login.
 | 
						|
									</p>
 | 
						|
								</div>
 | 
						|
							</div>
 | 
						|
						</div>
 | 
						|
					</div>
 | 
						|
 | 
						|
					{error && (
 | 
						|
						<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
 | 
						|
							<p className="text-sm text-danger-700 dark:text-danger-300">
 | 
						|
								{error}
 | 
						|
							</p>
 | 
						|
						</div>
 | 
						|
					)}
 | 
						|
 | 
						|
					<div className="flex justify-end space-x-3">
 | 
						|
						<button
 | 
						|
							type="button"
 | 
						|
							onClick={handleClose}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-secondary-700 dark:text-secondary-200 bg-white dark:bg-secondary-700 border border-secondary-300 dark:border-secondary-600 rounded-md hover:bg-secondary-50 dark:hover:bg-secondary-600"
 | 
						|
						>
 | 
						|
							Cancel
 | 
						|
						</button>
 | 
						|
						<button
 | 
						|
							type="submit"
 | 
						|
							disabled={isLoading}
 | 
						|
							className="px-4 py-2 text-sm font-medium text-white bg-primary-600 border border-transparent rounded-md hover:bg-primary-700 disabled:opacity-50 flex items-center"
 | 
						|
						>
 | 
						|
							{isLoading && (
 | 
						|
								<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
 | 
						|
							)}
 | 
						|
							{isLoading ? "Resetting..." : "Reset Password"}
 | 
						|
						</button>
 | 
						|
					</div>
 | 
						|
				</form>
 | 
						|
			</div>
 | 
						|
		</div>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
export default UsersTab;
 |