'use client'; import { useState, useEffect, useCallback, Suspense, useRef } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { LoadingSpinner } from '@/components/common/LoadingSpinner'; import { useRouter, useSearchParams } from 'next/navigation'; import { getProfileSettings, updateProfileSettings, updateProfile, searchCitiesFromCSV, loadTelegramAvatar, type ProfileSettings, type CityOption, } from '@/api/profile'; import { getNotificationPreferences, updateNotificationPreferences, type NotificationPreference, } from '@/api/notifications'; import { getReferralStats } from '@/api/referrals'; import { formatPhoneForDisplay, parsePhoneFromInput } from '@/lib/phone-utils'; import { ProfilePaymentTab } from '@/components/profile/ProfilePaymentTab'; import { NotificationSettingsSection } from '@/components/profile/NotificationSettingsSection'; import { ParentChildNotificationSettings } from '@/components/profile/ParentChildNotificationSettings'; import { TelegramSection } from '@/components/profile/TelegramSection'; import { Switch } from '@/components/common/Switch'; function getAvatarUrl(user: { avatar_url?: string | null; avatar?: string | null } | null): string | null { if (!user) return null; const url = user.avatar_url || user.avatar; if (!url) return null; if (url.startsWith('http')) return url; const base = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8123` : ''; return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`; } const ROLE_LABELS: Record = { mentor: 'Ментор', client: 'Студент', parent: 'Родитель', }; /** Имя/фамилия: только латиница ИЛИ только кириллица, без смешивания. Алфавит задаётся первым символом. */ function filterNameInput(value: string): string { if (!value.trim()) return value.replace(/[^a-zA-ZА-Яа-яЁё]/g, ''); const first = value[0]; if (/[a-zA-Z]/.test(first)) return value.replace(/[^a-zA-Z]/g, ''); if (/[А-Яа-яЁё]/.test(first)) return value.replace(/[^А-Яа-яЁё]/g, ''); return value.replace(/[^a-zA-ZА-Яа-яЁё]/g, ''); } type TabId = 'profile' | 'payment'; function ProfilePage() { const { user, loading: authLoading, logout, refreshUser } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const tabParam = searchParams?.get('tab'); const activeTab: TabId = tabParam === 'payment' ? 'payment' : 'profile'; const avatarUrl = getAvatarUrl(user); const [settings, setSettings] = useState(null); const [notificationPrefs, setNotificationPrefs] = useState(null); const [formData, setFormData] = useState({ first_name: '', last_name: '', phone: '', email: '' }); const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(null); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [saving, setSaving] = useState(false); const [loadError, setLoadError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); const [citySearchQuery, setCitySearchQuery] = useState(''); const [citySearchResults, setCitySearchResults] = useState([]); const [isCityInputFocused, setIsCityInputFocused] = useState(false); const [isSearchingCities, setIsSearchingCities] = useState(false); const [avatarHovered, setAvatarHovered] = useState(false); const [loadingTelegramAvatar, setLoadingTelegramAvatar] = useState(false); const [referralPoints, setReferralPoints] = useState(null); const cityInputRef = useRef(null); const dropdownRef = useRef(null); const saveSettingsAndNotificationsRef = useRef<() => Promise>(() => Promise.resolve()); const settingsSaveTimeoutRef = useRef | null>(null); const loadProfile = useCallback(async () => { if (!user) return; setFormData({ first_name: user.first_name || '', last_name: user.last_name || '', phone: user.phone || '', email: user.email || '', }); try { const [settingsRes, notifRes, refStats] = await Promise.all([ getProfileSettings().catch(() => null), getNotificationPreferences(), getReferralStats().catch(() => null), ]); if (refStats?.total_points != null) setReferralPoints(refStats.total_points); setSettings(settingsRes || null); const defaultPrefs = { enabled: true, email_enabled: true, telegram_enabled: false, in_app_enabled: true, }; setNotificationPrefs((prev) => notifRes ?? prev ?? defaultPrefs); if (settingsRes) { if (!settingsRes.preferences?.timezone) { settingsRes.preferences = { ...settingsRes.preferences, timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'Europe/Moscow', }; } if (settingsRes.preferences?.city) { setCitySearchQuery(settingsRes.preferences.city); } } } catch (e) { setLoadError('Не удалось загрузить настройки'); } }, [user]); useEffect(() => { if (tabParam === 'analytics') router.replace('/analytics'); if (tabParam === 'referrals') router.replace('/referrals'); }, [tabParam, router]); const handleChangePreference = useCallback((field: 'city' | 'timezone', value: string) => { setSettings((prev) => { if (!prev) return prev; return { ...prev, preferences: { ...prev.preferences, [field]: value }, }; }); }, []); const handleCitySearch = useCallback(async (query: string) => { if (query.trim().length < 2) { setCitySearchResults([]); return; } setIsSearchingCities(true); try { const results = await searchCitiesFromCSV(query.trim(), 20); setCitySearchResults(results); } catch { setCitySearchResults([]); } finally { setIsSearchingCities(false); } }, []); useEffect(() => { const t = setTimeout(() => { if (citySearchQuery.trim().length >= 2) handleCitySearch(citySearchQuery); else setCitySearchResults([]); }, 300); return () => clearTimeout(t); }, [citySearchQuery, handleCitySearch]); useEffect(() => { loadProfile(); }, [loadProfile]); const handleAvatarChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !user) return; if (file.size > 5 * 1024 * 1024) { setLoadError('Размер файла не должен превышать 5 МБ'); return; } const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']; if (!allowedTypes.includes(file.type)) { setLoadError('Поддерживаются только форматы: JPEG, PNG, WebP, GIF'); return; } const objectUrl = URL.createObjectURL(file); setAvatarPreview(objectUrl); setAvatarFile(file); setLoadError(null); try { setUploadingAvatar(true); const updatedUser = await updateProfile({ avatar: file }); await refreshUser(); const newAvatarUrl = getAvatarUrl(updatedUser); if (newAvatarUrl) { URL.revokeObjectURL(objectUrl); setAvatarPreview(newAvatarUrl); setAvatarFile(null); } else { setAvatarPreview(objectUrl); } } catch (err) { console.error(err); setLoadError('Не удалось загрузить аватар. Нажмите «Сохранить» для повторной попытки.'); // objectUrl не отзываем — оставляем preview, avatarFile сохранён для кнопки «Сохранить» } finally { setUploadingAvatar(false); e.target.value = ''; } }; const handleAvatarDelete = async () => { if (!user) return; try { await updateProfile({ avatar: null }); await refreshUser(); setAvatarFile(null); setAvatarPreview(null); } catch (err) { console.error(err); } }; const handleLoadTelegramAvatar = async () => { if (!user?.telegram_id) return; try { setLoadingTelegramAvatar(true); setLoadError(null); const updatedUser = await loadTelegramAvatar(); await refreshUser(); const newUrl = getAvatarUrl(updatedUser); if (newUrl) { setAvatarPreview(newUrl); setAvatarFile(null); } setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 3000); } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || (err instanceof Error ? err.message : 'Не удалось загрузить аватар из Telegram'); setLoadError(typeof msg === 'string' ? msg : 'Не удалось загрузить аватар из Telegram'); } finally { setLoadingTelegramAvatar(false); } }; const saveProfileFields = useCallback(async () => { if (!user) return; try { const updatedUser = await updateProfile({ first_name: formData.first_name, last_name: formData.last_name, phone: formData.phone, email: formData.email, }); if (updatedUser) { setFormData({ first_name: updatedUser.first_name ?? formData.first_name, last_name: updatedUser.last_name ?? formData.last_name, phone: updatedUser.phone ?? formData.phone, email: updatedUser.email ?? formData.email, }); } setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 3000); } catch (err) { console.error(err); setLoadError('Не удалось сохранить имя или телефон.'); } }, [user, formData.first_name, formData.last_name, formData.phone]); const saveSettingsAndNotifications = useCallback(async () => { if (!user || !settings) return; try { setSaving(true); setLoadError(null); const prefsToSave = { ...settings, preferences: { ...settings?.preferences, city: citySearchQuery.trim() || settings?.preferences?.city, }, ...(user.role === 'mentor' && settings?.mentor_homework_ai !== undefined && { mentor_homework_ai: settings.mentor_homework_ai, }), }; await updateProfileSettings(prefsToSave); if (notificationPrefs) { const updated = await updateNotificationPreferences(notificationPrefs); if (updated) setNotificationPrefs(updated); } setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 3000); } catch (err) { console.error(err); setLoadError('Не удалось сохранить настройки.'); } finally { setSaving(false); } }, [user, settings, citySearchQuery, notificationPrefs]); saveSettingsAndNotificationsRef.current = saveSettingsAndNotifications; const scheduleSettingsSave = useCallback(() => { if (settingsSaveTimeoutRef.current) clearTimeout(settingsSaveTimeoutRef.current); settingsSaveTimeoutRef.current = setTimeout(() => { settingsSaveTimeoutRef.current = null; saveSettingsAndNotificationsRef.current(); }, 800); }, []); useEffect(() => { return () => { if (settingsSaveTimeoutRef.current) clearTimeout(settingsSaveTimeoutRef.current); }; }, []); const handleProfileFieldBlur = useCallback(() => { saveProfileFields(); }, [saveProfileFields]); const handleSave = async (e: React.FormEvent) => { e.preventDefault(); if (!user) return; setSaving(true); setLoadError(null); setSaveSuccess(false); try { const updatedUser = await updateProfile({ first_name: formData.first_name, last_name: formData.last_name, phone: formData.phone, email: formData.email, ...(avatarFile instanceof File && { avatar: avatarFile }), }); if (updatedUser) { setFormData({ first_name: updatedUser.first_name ?? formData.first_name, last_name: updatedUser.last_name ?? formData.last_name, phone: updatedUser.phone ?? formData.phone, email: updatedUser.email ?? formData.email, }); } const prefsToSave = { ...settings, preferences: { ...settings?.preferences, city: citySearchQuery.trim() || settings?.preferences?.city, }, ...(user.role === 'mentor' && settings?.mentor_homework_ai !== undefined && { mentor_homework_ai: settings.mentor_homework_ai, }), }; if (prefsToSave) await updateProfileSettings(prefsToSave); if (notificationPrefs) { const updatedPrefs = await updateNotificationPreferences(notificationPrefs); if (updatedPrefs) setNotificationPrefs(updatedPrefs); } setAvatarFile(null); setAvatarPreview(null); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 3000); } catch (err) { console.error(err); if (avatarFile instanceof File) { setLoadError('Не удалось сохранить аватар. Проверьте формат и размер (до 5 МБ).'); } } finally { setSaving(false); } }; const handleLogout = () => { logout(); router.push('/login'); }; if (authLoading || !user) { return (
); } return (
{/* Левая карточка — Мой профиль */}
{/* Аватар — квадрат, заполняет пространство */}
setAvatarHovered(true)} onMouseLeave={() => setAvatarHovered(false)} style={{ position: 'relative', width: '100%', aspectRatio: '1', overflow: 'hidden', background: 'var(--md-sys-color-surface-variant)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--md-sys-color-on-surface-variant)', fontSize: 'clamp(32px, 10vw, 64px)', fontWeight: 600, cursor: 'pointer', }} > {avatarPreview || avatarUrl ? ( <> {uploadingAvatar && (
)} ) : ( user.first_name?.charAt(0) || user.email?.charAt(0) || 'У' )}
{user?.telegram_id && ( )} {(avatarPreview || avatarUrl) && ( )}
{/* Поля — 2 колонки */}
setFormData((p) => ({ ...p, first_name: filterNameInput(e.target.value) }))} onBlur={handleProfileFieldBlur} placeholder="Имя" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, border: '1px solid #E6E6E6', background: '#FAFAFA', fontSize: 15, color: '#282C32', boxSizing: 'border-box', }} />
setFormData((p) => ({ ...p, last_name: filterNameInput(e.target.value) }))} onBlur={handleProfileFieldBlur} placeholder="Фамилия" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, border: '1px solid #E6E6E6', background: '#FAFAFA', fontSize: 15, color: '#282C32', boxSizing: 'border-box', }} />
setFormData((p) => ({ ...p, phone: parsePhoneFromInput(e.target.value) }))} onBlur={handleProfileFieldBlur} placeholder="+7 (999) 123-45-67" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, border: '1px solid #E6E6E6', background: '#FAFAFA', fontSize: 15, color: '#282C32', boxSizing: 'border-box', }} />
setFormData((p) => ({ ...p, email: e.target.value.toLowerCase().trim() }))} onBlur={handleProfileFieldBlur} placeholder="Email" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, border: '1px solid #E6E6E6', background: formData.email?.endsWith('@platform.local') ? '#FFF9C4' : '#FAFAFA', fontSize: 15, color: '#282C32', boxSizing: 'border-box', }} /> {formData.email?.endsWith('@platform.local') && (

Установите реальный Email для доступа к аккаунту

)}
{/* Город */}
setCitySearchQuery(e.target.value)} onFocus={() => { setIsCityInputFocused(true); if (citySearchQuery.trim().length >= 2) handleCitySearch(citySearchQuery); }} onBlur={() => setTimeout(() => setIsCityInputFocused(false), 200)} placeholder="Введите город" autoComplete="off" style={{ width: '100%', padding: '14px 16px', borderRadius: 12, border: '1px solid #E6E6E6', background: '#FAFAFA', fontSize: 15, color: '#282C32', boxSizing: 'border-box', }} /> {isSearchingCities && (
)} {isCityInputFocused && citySearchResults.length > 0 && (
{citySearchResults.map((city, idx) => ( ))}
)}

Выберите город для определения часового пояса

{/* Часовой пояс */}

Устанавливается автоматически по городу

refreshUser()} /> {loadError && (

{loadError}

)}
{/* Правая часть — настройки уведомлений или контент таба */}
{activeTab === 'profile' && (

Настройки уведомлений

{ setNotificationPrefs(p); scheduleSettingsSave(); }} userRole={user?.role} hasTelegram={!!user?.telegram_id} disabled={saving} /> {user?.role === 'mentor' && (
ПРОВЕРКА ДЗ ЧЕРЕЗ ИИ
Режим
Вкл
Доверять ИИ (черновик)
ИИ выставит оценку и комментарий и сохранит как черновик. Вы сможете отредактировать и опубликовать.
{ setSettings((prev) => ({ ...prev!, mentor_homework_ai: { ...prev?.mentor_homework_ai, ai_trust_draft: v, ai_trust_publish: v ? false : (prev?.mentor_homework_ai?.ai_trust_publish ?? false), }, })); scheduleSettingsSave(); }} disabled={saving} size="compact" />
Полностью доверять ИИ
ИИ выставит оценку и комментарий и опубликует результат студенту сразу после сдачи ДЗ.
{ setSettings((prev) => ({ ...prev!, mentor_homework_ai: { ...prev?.mentor_homework_ai, ai_trust_publish: v, ai_trust_draft: v ? false : (prev?.mentor_homework_ai?.ai_trust_draft ?? false), }, })); scheduleSettingsSave(); }} disabled={saving} size="compact" />
)}
)} {activeTab !== 'profile' && (
{activeTab === 'payment' && }
)}
); } export default function ProfilePageWrapper() { return ( }> ); }