383 lines
16 KiB
TypeScript
383 lines
16 KiB
TypeScript
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";
|
||
|
||
|
||
const initialFormData: UserFormData = {
|
||
id: '',
|
||
email: '',
|
||
password: '',
|
||
displayName: '',
|
||
bio: '',
|
||
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,
|
||
bio: user.bio,
|
||
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="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||
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="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||
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="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||
required={!showEditModal}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Биографические данные
|
||
</label>
|
||
<textarea
|
||
value={formData.bio}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, bio: e.target.value }))}
|
||
rows={4}
|
||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
||
/>
|
||
</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>
|
||
);
|
||
} |