534 lines
24 KiB
TypeScript
534 lines
24 KiB
TypeScript
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|||
|
|
import { X, User, Mail, Phone, Calendar, Lock, Settings, Image as ImageIcon, Loader2, MessageCircle } from 'lucide-react';
|
|||
|
|
import { User as UserType, UserRole } from '../types';
|
|||
|
|
import { backendApi } from '../services/apiClient';
|
|||
|
|
|
|||
|
|
const ROLE_NAMES: Record<UserRole, string> = {
|
|||
|
|
DIRECTOR: 'Директор',
|
|||
|
|
ENGINEER: 'Гл. Инженер',
|
|||
|
|
MASTER: 'Мастер',
|
|||
|
|
LAWYER: 'Юрист',
|
|||
|
|
FINANCIER: 'Финансист',
|
|||
|
|
HR_MANAGER: 'HR-менеджер',
|
|||
|
|
PR_MANAGER: 'PR-менеджер'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const DEFAULT_AVATAR = 'https://api.dicebear.com/7.x/avataaars/svg?seed=default';
|
|||
|
|
|
|||
|
|
type TabId = 'profile' | 'photo' | 'security' | 'preferences' | 'messengers';
|
|||
|
|
|
|||
|
|
interface ProfileSettingsModalProps {
|
|||
|
|
user: UserType;
|
|||
|
|
onClose: () => void;
|
|||
|
|
onSave: (user: UserType) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const ProfileSettingsModal: React.FC<ProfileSettingsModalProps> = ({ user, onClose, onSave }) => {
|
|||
|
|
const [activeTab, setActiveTab] = useState<TabId>('profile');
|
|||
|
|
const [form, setForm] = useState({
|
|||
|
|
name: user.name || '',
|
|||
|
|
email: user.email || '',
|
|||
|
|
phone: user.phone || '',
|
|||
|
|
birthDate: user.birthDate || '',
|
|||
|
|
language: user.language || 'ru',
|
|||
|
|
theme: user.theme || 'light',
|
|||
|
|
notificationEmail: user.notificationEmail !== false,
|
|||
|
|
notificationPush: user.notificationPush !== false,
|
|||
|
|
});
|
|||
|
|
const [passwordForm, setPasswordForm] = useState({
|
|||
|
|
oldPassword: '',
|
|||
|
|
newPassword: '',
|
|||
|
|
confirmPassword: '',
|
|||
|
|
});
|
|||
|
|
const [messengers, setMessengers] = useState<Array<{ messenger: 'Max' | 'Telegram'; login: string }>>(
|
|||
|
|
user.messengerLogins || []
|
|||
|
|
);
|
|||
|
|
const [newMessenger, setNewMessenger] = useState<{ messenger: 'Max' | 'Telegram'; login: string }>({
|
|||
|
|
messenger: 'Telegram',
|
|||
|
|
login: ''
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|||
|
|
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
|||
|
|
const [isDragging, setIsDragging] = useState(false);
|
|||
|
|
const [isSaving, setIsSaving] = useState(false);
|
|||
|
|
const [isUploadingPhoto, setIsUploadingPhoto] = useState(false);
|
|||
|
|
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
|||
|
|
const [error, setError] = useState<string | null>(null);
|
|||
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|||
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|||
|
|
|
|||
|
|
const getAvatarUrl = () => {
|
|||
|
|
if (photoPreview) return photoPreview;
|
|||
|
|
if (!user.avatar) return DEFAULT_AVATAR;
|
|||
|
|
if (user.avatar.startsWith('http')) return user.avatar;
|
|||
|
|
const base = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/api\/?$/, '') || window.location.origin;
|
|||
|
|
return `${base}${user.avatar.startsWith('/') ? '' : '/'}${user.avatar}`;
|
|||
|
|
};
|
|||
|
|
const avatarUrl = getAvatarUrl();
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setForm({
|
|||
|
|
name: user.name || '',
|
|||
|
|
email: user.email || '',
|
|||
|
|
phone: user.phone || '',
|
|||
|
|
birthDate: user.birthDate || '',
|
|||
|
|
language: user.language || 'ru',
|
|||
|
|
theme: user.theme || 'light',
|
|||
|
|
notificationEmail: user.notificationEmail !== false,
|
|||
|
|
notificationPush: user.notificationPush !== false,
|
|||
|
|
});
|
|||
|
|
setMessengers(user.messengerLogins || []);
|
|||
|
|
}, [user]);
|
|||
|
|
|
|||
|
|
const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
|
|||
|
|
{ id: 'profile', label: 'Основная информация', icon: <User className="w-4 h-4" /> },
|
|||
|
|
{ id: 'photo', label: 'Фото профиля', icon: <ImageIcon className="w-4 h-4" /> },
|
|||
|
|
{ id: 'security', label: 'Безопасность', icon: <Lock className="w-4 h-4" /> },
|
|||
|
|
{ id: 'preferences', label: 'Настройки', icon: <Settings className="w-4 h-4" /> },
|
|||
|
|
{ id: 'messengers', label: 'Мессенджеры', icon: <MessageCircle className="w-4 h-4" /> },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const handleSaveProfile = async () => {
|
|||
|
|
try {
|
|||
|
|
setIsSaving(true);
|
|||
|
|
setError(null);
|
|||
|
|
const updated = await backendApi.updateProfile({
|
|||
|
|
name: form.name.trim(),
|
|||
|
|
email: form.email.trim() || undefined,
|
|||
|
|
phone: form.phone.trim() || undefined,
|
|||
|
|
birthDate: form.birthDate || undefined,
|
|||
|
|
});
|
|||
|
|
onSave(updated);
|
|||
|
|
setSuccess('Профиль сохранён');
|
|||
|
|
setTimeout(() => setSuccess(null), 3000);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка сохранения');
|
|||
|
|
} finally {
|
|||
|
|
setIsSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSavePreferences = async () => {
|
|||
|
|
try {
|
|||
|
|
setIsSaving(true);
|
|||
|
|
setError(null);
|
|||
|
|
await backendApi.updatePreferences({
|
|||
|
|
language: form.language,
|
|||
|
|
theme: form.theme,
|
|||
|
|
notificationEmail: form.notificationEmail,
|
|||
|
|
notificationPush: form.notificationPush,
|
|||
|
|
});
|
|||
|
|
const updated = await backendApi.getMe();
|
|||
|
|
onSave(updated);
|
|||
|
|
setSuccess('Настройки сохранены');
|
|||
|
|
setTimeout(() => setSuccess(null), 3000);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка сохранения');
|
|||
|
|
} finally {
|
|||
|
|
setIsSaving(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePhotoSelect = (file: File | null) => {
|
|||
|
|
if (!file) {
|
|||
|
|
setPhotoFile(null);
|
|||
|
|
setPhotoPreview(null);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!file.type.startsWith('image/')) {
|
|||
|
|
setError('Выберите изображение (JPG, PNG, GIF, WEBP)');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (file.size > 5 * 1024 * 1024) {
|
|||
|
|
setError('Размер файла не должен превышать 5MB');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setError(null);
|
|||
|
|
setPhotoFile(file);
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onloadend = () => setPhotoPreview(reader.result as string);
|
|||
|
|
reader.readAsDataURL(file);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePhotoUpload = async () => {
|
|||
|
|
if (!photoFile) return;
|
|||
|
|
try {
|
|||
|
|
setIsUploadingPhoto(true);
|
|||
|
|
setError(null);
|
|||
|
|
const { photoUrl } = await backendApi.uploadProfilePhoto(photoFile);
|
|||
|
|
const updated = await backendApi.getMe();
|
|||
|
|
onSave({ ...updated, avatar: photoUrl });
|
|||
|
|
setPhotoFile(null);
|
|||
|
|
setPhotoPreview(null);
|
|||
|
|
setSuccess('Фото загружено');
|
|||
|
|
setTimeout(() => setSuccess(null), 3000);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка загрузки фото');
|
|||
|
|
} finally {
|
|||
|
|
setIsUploadingPhoto(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeletePhoto = async () => {
|
|||
|
|
try {
|
|||
|
|
setIsUploadingPhoto(true);
|
|||
|
|
setError(null);
|
|||
|
|
await backendApi.deleteProfilePhoto();
|
|||
|
|
const updated = await backendApi.getMe();
|
|||
|
|
onSave(updated);
|
|||
|
|
setPhotoFile(null);
|
|||
|
|
setPhotoPreview(null);
|
|||
|
|
setSuccess('Фото удалено');
|
|||
|
|
setTimeout(() => setSuccess(null), 3000);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка удаления');
|
|||
|
|
} finally {
|
|||
|
|
setIsUploadingPhoto(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleChangePassword = async () => {
|
|||
|
|
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
|||
|
|
setError('Пароли не совпадают');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (passwordForm.newPassword.length < 8) {
|
|||
|
|
setError('Пароль должен содержать не менее 8 символов');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!/^(?=.*[a-zA-Z])(?=.*\d)/.test(passwordForm.newPassword)) {
|
|||
|
|
setError('Пароль должен содержать буквы и цифры');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
setIsChangingPassword(true);
|
|||
|
|
setError(null);
|
|||
|
|
await backendApi.changePassword(passwordForm.oldPassword, passwordForm.newPassword);
|
|||
|
|
setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' });
|
|||
|
|
setSuccess('Пароль успешно изменён');
|
|||
|
|
setTimeout(() => setSuccess(null), 3000);
|
|||
|
|
} catch (err: any) {
|
|||
|
|
setError(err.message || 'Ошибка смены пароля');
|
|||
|
|
} finally {
|
|||
|
|
setIsChangingPassword(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDrop = (e: React.DragEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
setIsDragging(false);
|
|||
|
|
const file = e.dataTransfer.files?.[0];
|
|||
|
|
if (file) handlePhotoSelect(file);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
setIsDragging(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDragLeave = () => setIsDragging(false);
|
|||
|
|
|
|||
|
|
const addMessenger = () => {
|
|||
|
|
if (!newMessenger.login.trim()) return;
|
|||
|
|
if (messengers.some(m => m.messenger === newMessenger.messenger)) {
|
|||
|
|
setError('Этот мессенджер уже добавлен');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setMessengers([...messengers, { ...newMessenger }]);
|
|||
|
|
setNewMessenger({ messenger: 'Telegram', login: '' });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const removeMessenger = (idx: number) => {
|
|||
|
|
setMessengers(messengers.filter((_, i) => i !== idx));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md animate-fade-in" onClick={onClose}>
|
|||
|
|
<div className="bg-white rounded-[2.5rem] w-full max-w-2xl max-h-[90vh] overflow-hidden shadow-2xl animate-slide-up flex flex-col" onClick={e => e.stopPropagation()}>
|
|||
|
|
<div className="p-6 border-b border-slate-200 flex-shrink-0">
|
|||
|
|
<div className="flex justify-between items-start">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-2xl font-black text-slate-900">Профиль и настройки</h3>
|
|||
|
|
<p className="text-xs text-slate-500 mt-1">Управление вашим аккаунтом</p>
|
|||
|
|
</div>
|
|||
|
|
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full">
|
|||
|
|
<X className="w-6 h-6 text-slate-400" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-1 mt-4 overflow-x-auto no-scrollbar">
|
|||
|
|
{tabs.map((tab) => (
|
|||
|
|
<button
|
|||
|
|
key={tab.id}
|
|||
|
|
onClick={() => setActiveTab(tab.id)}
|
|||
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors whitespace-nowrap ${
|
|||
|
|
activeTab === tab.id ? 'bg-primary-100 text-primary-700' : 'text-slate-600 hover:bg-slate-100'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
{tab.icon}
|
|||
|
|
{tab.label}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
|||
|
|
{error && (
|
|||
|
|
<div className="p-3 bg-red-50 border border-red-200 rounded-xl text-sm text-red-700">
|
|||
|
|
{error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{success && (
|
|||
|
|
<div className="p-3 bg-green-50 border border-green-200 rounded-xl text-sm text-green-700">
|
|||
|
|
{success}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === 'profile' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">ФИО</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={form.name}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="Иванов Иван Иванович"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Email</label>
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={form.email}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, email: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="email@example.com"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Телефон</label>
|
|||
|
|
<input
|
|||
|
|
type="tel"
|
|||
|
|
value={form.phone}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, phone: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="+7 (999) 123-45-67"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Дата рождения</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={form.birthDate}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, birthDate: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-slate-500">Роль: {ROLE_NAMES[user.role]}</p>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSaveProfile}
|
|||
|
|
disabled={isSaving}
|
|||
|
|
className="w-full px-4 py-2.5 bg-primary-600 hover:bg-primary-700 disabled:bg-slate-400 text-white font-bold text-sm rounded-xl transition-colors flex items-center justify-center gap-2"
|
|||
|
|
>
|
|||
|
|
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
|||
|
|
Сохранить
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === 'photo' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="flex flex-col items-center gap-4">
|
|||
|
|
<div
|
|||
|
|
onDrop={handleDrop}
|
|||
|
|
onDragOver={handleDragOver}
|
|||
|
|
onDragLeave={handleDragLeave}
|
|||
|
|
className={`w-32 h-32 rounded-full border-2 border-dashed flex items-center justify-center overflow-hidden transition-colors ${
|
|||
|
|
isDragging ? 'border-primary-500 bg-primary-50' : 'border-slate-200 bg-slate-50'
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<img
|
|||
|
|
src={avatarUrl}
|
|||
|
|
alt=""
|
|||
|
|
className="w-full h-full object-cover"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<input
|
|||
|
|
ref={fileInputRef}
|
|||
|
|
type="file"
|
|||
|
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
|||
|
|
className="hidden"
|
|||
|
|
onChange={(e) => handlePhotoSelect(e.target.files?.[0] || null)}
|
|||
|
|
/>
|
|||
|
|
<div className="flex gap-3">
|
|||
|
|
<button
|
|||
|
|
onClick={() => fileInputRef.current?.click()}
|
|||
|
|
className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-medium text-sm rounded-xl"
|
|||
|
|
>
|
|||
|
|
Выбрать файл
|
|||
|
|
</button>
|
|||
|
|
{photoFile && (
|
|||
|
|
<button
|
|||
|
|
onClick={handlePhotoUpload}
|
|||
|
|
disabled={isUploadingPhoto}
|
|||
|
|
className="px-4 py-2 bg-primary-600 hover:bg-primary-700 disabled:bg-slate-400 text-white font-medium text-sm rounded-xl flex items-center gap-2"
|
|||
|
|
>
|
|||
|
|
{isUploadingPhoto ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
|||
|
|
Загрузить
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
{(user.avatar || photoPreview) && (
|
|||
|
|
<button
|
|||
|
|
onClick={handleDeletePhoto}
|
|||
|
|
disabled={isUploadingPhoto}
|
|||
|
|
className="px-4 py-2 bg-red-50 hover:bg-red-100 text-red-600 font-medium text-sm rounded-xl"
|
|||
|
|
>
|
|||
|
|
Удалить
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-slate-500">JPG, PNG, GIF, WEBP до 5MB</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === 'security' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Текущий пароль</label>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={passwordForm.oldPassword}
|
|||
|
|
onChange={(e) => setPasswordForm(f => ({ ...f, oldPassword: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="••••••••"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Новый пароль</label>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={passwordForm.newPassword}
|
|||
|
|
onChange={(e) => setPasswordForm(f => ({ ...f, newPassword: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="Минимум 8 символов, буквы и цифры"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Подтвердите новый пароль</label>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={passwordForm.confirmPassword}
|
|||
|
|
onChange={(e) => setPasswordForm(f => ({ ...f, confirmPassword: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
placeholder="••••••••"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
onClick={handleChangePassword}
|
|||
|
|
disabled={isChangingPassword || !passwordForm.oldPassword || !passwordForm.newPassword}
|
|||
|
|
className="w-full px-4 py-2.5 bg-primary-600 hover:bg-primary-700 disabled:bg-slate-400 text-white font-bold text-sm rounded-xl transition-colors flex items-center justify-center gap-2"
|
|||
|
|
>
|
|||
|
|
{isChangingPassword ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
|||
|
|
Сменить пароль
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === 'preferences' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Язык</label>
|
|||
|
|
<select
|
|||
|
|
value={form.language}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, language: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="ru">Русский</option>
|
|||
|
|
<option value="en">English</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-xs font-bold text-slate-700 mb-1">Тема</label>
|
|||
|
|
<select
|
|||
|
|
value={form.theme}
|
|||
|
|
onChange={(e) => setForm(f => ({ ...f, theme: e.target.value }))}
|
|||
|
|
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|||
|
|
>
|
|||
|
|
<option value="light">Светлая</option>
|
|||
|
|
<option value="dark">Тёмная</option>
|
|||
|
|
<option value="system">Системная</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between py-2">
|
|||
|
|
<span className="text-sm font-medium text-slate-700">Email уведомления</span>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setForm(f => ({ ...f, notificationEmail: !f.notificationEmail }))}
|
|||
|
|
className={`w-12 h-6 rounded-full transition-colors ${form.notificationEmail ? 'bg-primary-600' : 'bg-slate-200'}`}
|
|||
|
|
>
|
|||
|
|
<span className={`block w-5 h-5 bg-white rounded-full shadow transition-transform ${form.notificationEmail ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center justify-between py-2">
|
|||
|
|
<span className="text-sm font-medium text-slate-700">Push уведомления</span>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setForm(f => ({ ...f, notificationPush: !f.notificationPush }))}
|
|||
|
|
className={`w-12 h-6 rounded-full transition-colors ${form.notificationPush ? 'bg-primary-600' : 'bg-slate-200'}`}
|
|||
|
|
>
|
|||
|
|
<span className={`block w-5 h-5 bg-white rounded-full shadow transition-transform ${form.notificationPush ? 'translate-x-6' : 'translate-x-0.5'}`} />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSavePreferences}
|
|||
|
|
disabled={isSaving}
|
|||
|
|
className="w-full px-4 py-2.5 bg-primary-600 hover:bg-primary-700 disabled:bg-slate-400 text-white font-bold text-sm rounded-xl transition-colors flex items-center justify-center gap-2"
|
|||
|
|
>
|
|||
|
|
{isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
|||
|
|
Сохранить настройки
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === 'messengers' && (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<p className="text-sm text-slate-600">Логины мессенджеров для связи (информационно)</p>
|
|||
|
|
{messengers.map((m, idx) => (
|
|||
|
|
<div key={idx} className="flex items-center gap-3 p-3 bg-slate-50 rounded-xl">
|
|||
|
|
<span className="text-sm font-medium text-slate-700">{m.messenger}:</span>
|
|||
|
|
<span className="flex-1 text-sm text-slate-600">{m.login}</span>
|
|||
|
|
<button onClick={() => removeMessenger(idx)} className="text-red-600 hover:text-red-700 text-sm">
|
|||
|
|
Удалить
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<select
|
|||
|
|
value={newMessenger.messenger}
|
|||
|
|
onChange={(e) => setNewMessenger(m => ({ ...m, messenger: e.target.value as 'Max' | 'Telegram' }))}
|
|||
|
|
className="px-3 py-2 border border-slate-200 rounded-xl text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="Telegram">Telegram</option>
|
|||
|
|
<option value="Max">Max</option>
|
|||
|
|
</select>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={newMessenger.login}
|
|||
|
|
onChange={(e) => setNewMessenger(m => ({ ...m, login: e.target.value }))}
|
|||
|
|
placeholder="@username"
|
|||
|
|
className="flex-1 px-3 py-2 border border-slate-200 rounded-xl text-sm"
|
|||
|
|
/>
|
|||
|
|
<button onClick={addMessenger} className="px-4 py-2 bg-primary-600 text-white font-medium text-sm rounded-xl">
|
|||
|
|
Добавить
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-xs text-slate-500">Мессенджеры пока только отображаются. Сохранение в профиль будет добавлено.</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|