russ_react/src/pages/UserManagementPage.tsx

370 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react';
import { Header } from '../components/Header';
import { AuthGuard } from '../components/AuthGuard';
import { UserFormData } from '../types/auth';
import { useUserManagement } from '../hooks/useUserManagement';
import { ImagePlus, X, UserPlus, Pencil } from 'lucide-react';
import { imageResolutions } from '../config/imageResolutions';
import { CategoryIds, CategoryTitles, CityIds, CityTitles } from "../types";
import axios from "axios";
import { useAuthStore } from "../stores/authStore";
//import { AuthorModal } from "./AuthorModal";
const initialFormData: UserFormData = {
id: '',
email: '',
password: '',
displayName: '',
avatarUrl: '/images/avatar.jpg'
};
export function UserManagementPage() {
const {
users,
selectedUser,
loading,
error,
setSelectedUser,
handlePermissionChange,
handleCityChange,
createUser,
updateUser,
// deleteUser
} = useUserManagement();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [formData, setFormData] = useState<UserFormData>(initialFormData);
const [formError, setFormError] = useState<string | null>(null);
const { user } = useAuthStore();
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>, userId: string) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const resolution = imageResolutions.find(r => r.id === 'thumbnail');
if (!resolution) throw new Error('Invalid resolution');
const formData = new FormData();
formData.append('file', file);
formData.append('resolutionId', resolution.id);
formData.append('folder', 'users/' + userId);
// Отправка запроса на сервер
const response = await axios.post('/api/images/upload-url', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${localStorage.getItem('token')}`, // Передача токена аутентификации
},
});
setFormData(prev => ({ ...prev, avatarUrl: response.data?.fileUrl }));
} catch (error) {
setFormError('Ошибка загрузки аватара. Повторите попытку.');
console.error('Ошибка загрузки:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
if (showCreateModal) {
await createUser(formData);
setShowCreateModal(false);
} else if (showEditModal && selectedUser) {
await updateUser(selectedUser.id, formData);
setShowEditModal(false);
}
setFormData(initialFormData);
} catch (error) {
setFormError(error instanceof Error ? error.message : 'Произошла ошибка');
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<AuthGuard>
<div className="min-h-screen bg-gray-50">
<Header />
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">
Управление пользователями
</h1>
<button
onClick={() => {
setFormData(initialFormData);
setShowCreateModal(true);
}}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<UserPlus className="h-5 w-5 mr-2" />
Новый пользователь
</button>
</div>
{error && (
<div className="mb-8 bg-red-50 text-red-700 p-4 rounded-md">
{error}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Users List */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">Пользователи</h2>
<ul className="space-y-2">
{users.map((user) => (
<li key={user.id}>
<div
className={`flex items-center p-3 rounded-md ${
selectedUser?.id === user.id
? 'bg-blue-50'
: 'hover:bg-gray-50'
}`}
>
<img
src={user.avatarUrl}
alt={user.displayName}
className="w-10 h-10 rounded-full mr-3"
/>
<div className="flex-1">
<div className="font-medium">{user.displayName}</div>
<div className="text-sm text-gray-500">{user.email}</div>
</div>
<button
onClick={() => {
setSelectedUser(user);
setFormData({
id: user.id,
email: user.email,
password: '',
displayName: user.displayName,
avatarUrl: user.avatarUrl
});
setShowEditModal(true);
}}
className="p-2 text-gray-400 hover:text-blue-600 rounded-full hover:bg-blue-50"
>
<Pencil size={16} />
</button>
</div>
</li>
))}
</ul>
</div>
{/* Permissions Editor */}
{selectedUser && (
<div className="md:col-span-2 bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Редактирование прав пользователя "{selectedUser.displayName}"
</h2>
<div className="space-y-6">
{/* Categories Permissions */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-4">
Права по категориям
</h3>
<div className="space-y-4">
{CategoryIds.map((categoryId) => (
<div key={categoryId} className="border rounded-lg p-4">
<h4 className="font-medium mb-2">{CategoryTitles[categoryId]}</h4>
<div className="space-y-2">
{(['create', 'edit', 'delete'] as const).map((action) => (
<label
key={action}
className="flex items-center space-x-2"
>
<input
type="checkbox"
checked={
selectedUser.permissions.categories[categoryId]?.[action] ?? false
}
onChange={(e) =>
handlePermissionChange(
categoryId,
action,
e.target.checked
)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 capitalize">
{action}
</span>
</label>
))}
</div>
</div>
))}
</div>
</div>
{/* Cities Access */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-4">
Разрешения для города
</h3>
<div className="space-y-2">
{CityIds.map((city) => (
<label
key={city}
className="flex items-center space-x-2"
>
<input
type="checkbox"
checked={selectedUser.permissions.cities.includes(city)}
onChange={(e) =>
handleCityChange(city, e.target.checked)
}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">{CityTitles[city]}</span>
</label>
))}
</div>
</div>
</div>
</div>
)}
</div>
</div>
</main>
{/* Create/Edit User Modal */}
{((showCreateModal || showEditModal) && user?.permissions.isAdmin) && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">
{showCreateModal ? 'Создание нового пользователя' : 'Редактирование'}
</h3>
<button
onClick={() => {
setShowCreateModal(false);
setShowEditModal(false);
setFormData(initialFormData);
setFormError(null);
}}
className="text-gray-400 hover:text-gray-500"
>
<X size={20} />
</button>
</div>
{formError && (
<div className="mb-4 bg-red-50 text-red-700 p-3 rounded-md text-sm">
{formError}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4" autoComplete="off">
<div>
<label className="block text-sm font-medium text-gray-700">
Аватар
</label>
<div className="mt-1 flex items-center space-x-4">
<img
src={formData.avatarUrl || '/images/avatar.jpg'}
alt="Аватар пользователя"
className="h-12 w-12 rounded-full object-cover"
/>
<label className="cursor-pointer inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
<ImagePlus size={16} className="mr-2" />
Сменить аватар
<input
type="file"
className="hidden"
accept="image/*"
onChange={(e) => handleAvatarUpload(e, formData.id)} // Передаем userId
/>
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Имя
</label>
<input
type="text"
value={formData.displayName}
onChange={(e) => setFormData(prev => ({ ...prev, displayName: e.target.value }))}
autoComplete="off"
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
E-mail
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
autoComplete="off"
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Пароль
</label>
<input
type="password"
value={formData.password}
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
autoComplete="new-password"
placeholder={showEditModal ? 'Оставьте пустым, чтобы не менять' : ''}
className="px-2 py-1 mt-1 block w-full rounded-lg border border-gray-300 shadow-sm hover:border-blue-300 focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all duration-200"
required={!showEditModal}
/>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
type="button"
onClick={() => {
setShowCreateModal(false);
setShowEditModal(false);
setFormData(initialFormData);
setFormError(null);
}}
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
Отмена
</button>
<button
type="submit"
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
{showCreateModal ? 'Создать' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
</AuthGuard>
);
}