'use client'; import { useRef, useState, useEffect, useCallback } from 'react'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; import Chip from '@mui/material/Chip'; import Stack from '@mui/material/Stack'; import Alert from '@mui/material/Alert'; import Paper from '@mui/material/Paper'; import Table from '@mui/material/Table'; import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import Switch from '@mui/material/Switch'; import Divider from '@mui/material/Divider'; import Tooltip from '@mui/material/Tooltip'; import TableRow from '@mui/material/TableRow'; import Snackbar from '@mui/material/Snackbar'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TextField from '@mui/material/TextField'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import LoadingButton from '@mui/lab/LoadingButton'; import CardContent from '@mui/material/CardContent'; import DialogTitle from '@mui/material/DialogTitle'; import Autocomplete from '@mui/material/Autocomplete'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import FormControlLabel from '@mui/material/FormControlLabel'; import CircularProgress from '@mui/material/CircularProgress'; import { paths } from 'src/routes/paths'; import axios, { resolveMediaUrl } from 'src/utils/axios'; import { unlinkTelegram, getTelegramStatus, getTelegramBotInfo, generateTelegramCode } from 'src/utils/telegram-api'; import { searchCities, deleteAvatar, updateProfile, loadTelegramAvatar, getProfileSettings, updateProfileSettings, getNotificationPreferences, updateNotificationPreferences, } from 'src/utils/profile-api'; import { DashboardContent } from 'src/layouts/dashboard'; import { Iconify } from 'src/components/iconify'; import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; import { useAuthContext } from 'src/auth/hooks'; // ---------------------------------------------------------------------- const ROLE_LABELS = { mentor: 'Ментор', client: 'Студент', parent: 'Родитель', }; const NOTIFICATION_TYPES = [ { key: 'lesson_created', label: 'Занятие создано' }, { key: 'lesson_cancelled', label: 'Занятие отменено' }, { key: 'lesson_reminder', label: 'Напоминание о занятии' }, { key: 'homework_assigned', label: 'ДЗ назначено' }, { key: 'homework_submitted', label: 'ДЗ сдано' }, { key: 'homework_reviewed', label: 'ДЗ проверено' }, { key: 'message_received', label: 'Новое сообщение' }, { key: 'subscription_expiring', label: 'Подписка истекает' }, { key: 'subscription_expired', label: 'Подписка истекла' }, ]; const PARENT_EXCLUDED_TYPES = [ 'lesson_created', 'lesson_cancelled', 'lesson_reminder', 'homework_assigned', 'homework_submitted', 'homework_reviewed', ]; const CHANNELS = [ { key: 'email', label: 'Email' }, { key: 'telegram', label: 'Telegram' }, { key: 'in_app', label: 'В приложении' }, ]; // ---------------------------------------------------------------------- const avatarSrc = (src) => resolveMediaUrl(src); // ---------------------------------------------------------------------- function TelegramSection({ onAvatarLoaded }) { const [status, setStatus] = useState(null); const [botInfo, setBotInfo] = useState(null); const [loading, setLoading] = useState(true); const [codeModal, setCodeModal] = useState(false); const [code, setCode] = useState(''); const [codeInstructions, setCodeInstructions] = useState(''); const [generating, setGenerating] = useState(false); const [unlinking, setUnlinking] = useState(false); const [loadingTgAvatar, setLoadingTgAvatar] = useState(false); const [copied, setCopied] = useState(false); const pollRef = useRef(null); const loadStatus = useCallback(async () => { try { const [s, b] = await Promise.all([ getTelegramStatus().catch(() => null), getTelegramBotInfo().catch(() => null), ]); setStatus(s); setBotInfo(b); } catch { // ignore } finally { setLoading(false); } }, []); useEffect(() => { loadStatus(); return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [loadStatus]); const handleGenerate = async () => { setGenerating(true); try { const res = await generateTelegramCode(); setCode(res.code || ''); setCodeInstructions(res.instructions || ''); setCodeModal(true); // Poll status every 5s after opening modal pollRef.current = setInterval(async () => { const s = await getTelegramStatus().catch(() => null); if (s?.linked) { setStatus(s); setCodeModal(false); clearInterval(pollRef.current); } }, 5000); } catch { // ignore } finally { setGenerating(false); } }; const handleCloseModal = () => { setCodeModal(false); if (pollRef.current) clearInterval(pollRef.current); loadStatus(); }; const handleUnlink = async () => { setUnlinking(true); try { await unlinkTelegram(); await loadStatus(); } catch { // ignore } finally { setUnlinking(false); } }; const handleLoadTgAvatar = async () => { setLoadingTgAvatar(true); try { await loadTelegramAvatar(); if (onAvatarLoaded) onAvatarLoaded(); } catch { // ignore } finally { setLoadingTgAvatar(false); } }; const handleCopy = () => { navigator.clipboard.writeText(`/link ${code}`).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; if (loading) { return ( ); } const botLink = botInfo?.link || (botInfo?.username ? `https://t.me/${botInfo.username}` : null); return ( <> {status?.linked ? ( Telegram подключён {status.telegram_username && ( @{status.telegram_username} )} } > Загрузить фото из Telegram } > Отвязать ) : ( Подключите Telegram для уведомлений } > Привязать Telegram )} {/* Code Modal */} Привязать Telegram 1. Откройте бота{' '} {botLink ? ( {botInfo?.username ? `@${botInfo.username}` : 'Telegram бот'} ) : ( 'Telegram бот' )}{' '} в Telegram 2. Отправьте боту команду: /link {code} {codeInstructions && ( {codeInstructions} )} Ожидаем подтверждения... ); } // ---------------------------------------------------------------------- const CHILD_NOTIFICATION_TYPES = [ { value: 'lesson_created', label: 'Создано занятие' }, { value: 'lesson_updated', label: 'Занятие обновлено' }, { value: 'lesson_cancelled', label: 'Занятие отменено' }, { value: 'lesson_rescheduled', label: 'Занятие перенесено' }, { value: 'lesson_reminder', label: 'Напоминание о занятии' }, { value: 'lesson_completed', label: 'Занятие завершено' }, { value: 'homework_assigned', label: 'Назначено домашнее задание' }, { value: 'homework_submitted', label: 'ДЗ сдано' }, { value: 'homework_reviewed', label: 'ДЗ проверено' }, { value: 'homework_returned', label: 'ДЗ возвращено на доработку' }, { value: 'homework_deadline_reminder', label: 'Напоминание о дедлайне ДЗ' }, { value: 'material_added', label: 'Добавлен материал' }, ]; function ParentChildNotifications() { const [children, setChildren] = useState([]); const [settings, setSettings] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState({}); const [expanded, setExpanded] = useState(null); const [error, setError] = useState(''); useEffect(() => { const load = async () => { try { setLoading(true); const res = await axios.get('/parent/dashboard/'); const raw = res.data?.children ?? []; const list = raw.map((item) => { const c = item.child ?? item; return { id: String(c.id), name: c.name || c.email || 'Ребёнок', avatar_url: c.avatar_url }; }); setChildren(list); const map = {}; await Promise.all(list.map(async (child) => { try { const r = await axios.get(`/notifications/parent-child-settings/for_child/?child_id=${child.id}`); map[child.id] = r.data; } catch { map[child.id] = { enabled: true, type_settings: {} }; } })); setSettings(map); } catch { setError('Не удалось загрузить настройки уведомлений для детей'); } finally { setLoading(false); } }; load(); }, []); const patch = async (childId, payload) => { try { setSaving((p) => ({ ...p, [childId]: true })); const r = await axios.patch(`/notifications/parent-child-settings/for_child/?child_id=${childId}`, payload); setSettings((p) => ({ ...p, [childId]: r.data })); } catch { setError('Не удалось сохранить'); } finally { setSaving((p) => ({ ...p, [childId]: false })); } }; if (loading) return ; if (children.length === 0) return null; return ( Уведомления по детям {error && {error}} {children.map((child) => { const s = settings[child.id] || { enabled: true, type_settings: {} }; const isExpanded = expanded === child.id; return ( {/* Header row */} setExpanded(isExpanded ? null : child.id)} > {child.name[0]?.toUpperCase()} {child.name} {s.enabled ? 'Включены' : 'Выключены'} e.stopPropagation()}> {saving[child.id] && } patch(child.id, { enabled: !s.enabled })} disabled={saving[child.id]} /> { e.stopPropagation(); setExpanded(isExpanded ? null : child.id); }} /> {/* Expanded type list */} {isExpanded && ( {CHILD_NOTIFICATION_TYPES.map((type) => { const isOn = s.type_settings[type.value] !== false; return ( {type.label} patch(child.id, { type_settings: { ...s.type_settings, [type.value]: !isOn } })} /> ); })} )} ); })} ); } // ---------------------------------------------------------------------- function NotificationMatrix({ prefs, onChange, role }) { const visibleTypes = role === 'parent' ? NOTIFICATION_TYPES.filter((t) => !PARENT_EXCLUDED_TYPES.includes(t.key)) : NOTIFICATION_TYPES; const getTypeValue = (typeKey, channelKey) => { const tp = prefs?.type_preferences; if (tp && tp[typeKey] && typeof tp[typeKey][channelKey] === 'boolean') { return tp[typeKey][channelKey]; } // Fallback to channel-level setting const channelMap = { email: 'email_enabled', telegram: 'telegram_enabled', in_app: 'in_app_enabled' }; return !!prefs?.[channelMap[channelKey]]; }; const handleToggle = (typeKey, channelKey) => { const current = getTypeValue(typeKey, channelKey); onChange({ type_preferences: { ...prefs?.type_preferences, [typeKey]: { ...(prefs?.type_preferences?.[typeKey] || {}), [channelKey]: !current, }, }, }); }; return ( Тип уведомления {CHANNELS.map((ch) => ( {ch.label} ))} {visibleTypes.map((type) => ( {type.label} {CHANNELS.map((ch) => ( handleToggle(type.key, ch.key)} disabled={!prefs?.enabled} /> ))} ))}
); } // ---------------------------------------------------------------------- export function AccountPlatformView() { const { user, checkUserSession } = useAuthContext(); // Profile fields const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState(''); const [avatarPreview, setAvatarPreview] = useState(null); const [avatarHovered, setAvatarHovered] = useState(false); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [deletingAvatar, setDeletingAvatar] = useState(false); // Settings const [settings, setSettings] = useState(null); const [settingsLoading, setSettingsLoading] = useState(true); const [settingsSaving, setSettingsSaving] = useState(false); // Notification prefs const [notifPrefs, setNotifPrefs] = useState(null); const [notifSaving, setNotifSaving] = useState(false); // City autocomplete const [cityQuery, setCityQuery] = useState(''); const [cityOptions, setCityOptions] = useState([]); const [citySearching, setCitySearching] = useState(false); const cityDebounceRef = useRef(null); // Auto-save debounce for settings const settingsSaveRef = useRef(null); // Snackbar const [snack, setSnack] = useState({ open: false, message: '', severity: 'success' }); // ---------------------------------------------------------------------- useEffect(() => { if (user) { setFirstName(user.first_name || ''); setLastName(user.last_name || ''); setEmail(user.email || ''); } }, [user]); useEffect(() => { async function load() { setSettingsLoading(true); try { const [s, n] = await Promise.all([ getProfileSettings().catch(() => null), getNotificationPreferences().catch(() => null), ]); setSettings(s); setNotifPrefs(n); if (s?.preferences?.city) setCityQuery(s.preferences.city); } finally { setSettingsLoading(false); } } load(); }, []); // ---------------------------------------------------------------------- // City search useEffect(() => { if (cityDebounceRef.current) clearTimeout(cityDebounceRef.current); if (!cityQuery || cityQuery.length < 2) { setCityOptions([]); return undefined; } cityDebounceRef.current = setTimeout(async () => { setCitySearching(true); const results = await searchCities(cityQuery, 20); setCityOptions(results); setCitySearching(false); }, 400); return () => { if (cityDebounceRef.current) clearTimeout(cityDebounceRef.current); }; }, [cityQuery]); // ---------------------------------------------------------------------- // Avatar const handleAvatarChange = async (e) => { const file = e.target.files?.[0]; if (!file) return; setAvatarPreview(URL.createObjectURL(file)); setUploadingAvatar(true); try { await updateProfile({ avatar: file }); if (checkUserSession) await checkUserSession(); showSnack('Аватар обновлён'); } catch { showSnack('Ошибка загрузки аватара', 'error'); } finally { setUploadingAvatar(false); } }; const handleDeleteAvatar = async () => { setDeletingAvatar(true); try { await deleteAvatar(); setAvatarPreview(null); if (checkUserSession) await checkUserSession(); showSnack('Аватар удалён'); } catch { showSnack('Ошибка удаления аватара', 'error'); } finally { setDeletingAvatar(false); } }; const handleTgAvatarLoaded = async () => { if (checkUserSession) await checkUserSession(); showSnack('Фото из Telegram загружено'); }; // ---------------------------------------------------------------------- // Profile field auto-save on blur const handleProfileBlur = useCallback(async (field, value) => { try { await updateProfile({ [field]: value }); if (checkUserSession) await checkUserSession(); } catch { showSnack('Ошибка сохранения', 'error'); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [checkUserSession]); // ---------------------------------------------------------------------- // Settings save (debounced) const scheduleSettingsSave = useCallback((newSettings) => { setSettings(newSettings); if (settingsSaveRef.current) clearTimeout(settingsSaveRef.current); settingsSaveRef.current = setTimeout(async () => { setSettingsSaving(true); try { await updateProfileSettings({ notifications: newSettings?.notifications, preferences: newSettings?.preferences, mentor_homework_ai: newSettings?.mentor_homework_ai, }); } catch { showSnack('Ошибка сохранения настроек', 'error'); } finally { setSettingsSaving(false); } }, 800); }, []); // ---------------------------------------------------------------------- // Notification prefs save (debounced) const notifSaveRef = useRef(null); const scheduleNotifSave = useCallback((updated) => { setNotifPrefs(updated); if (notifSaveRef.current) clearTimeout(notifSaveRef.current); notifSaveRef.current = setTimeout(async () => { setNotifSaving(true); try { await updateNotificationPreferences(updated); } catch { showSnack('Ошибка сохранения уведомлений', 'error'); } finally { setNotifSaving(false); } }, 800); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleNotifChange = (partial) => { scheduleNotifSave({ ...notifPrefs, ...partial }); }; // ---------------------------------------------------------------------- const showSnack = (message, severity = 'success') => { setSnack({ open: true, message, severity }); }; // ---------------------------------------------------------------------- const currentAvatar = avatarPreview || avatarSrc(user?.avatar); const displayName = `${firstName} ${lastName}`.trim() || user?.email || ''; const roleLabel = ROLE_LABELS[user?.role] || user?.role || ''; return ( {/* ─── LEFT COLUMN ─── */} {/* Avatar card */} {/* Square avatar with hover overlay */} setAvatarHovered(true)} onMouseLeave={() => setAvatarHovered(false)} > {!currentAvatar && displayName[0]?.toUpperCase()} {/* Hover overlay */} {avatarHovered && ( {uploadingAvatar ? ( ) : ( )} {currentAvatar && ( {deletingAvatar ? ( ) : ( )} )} )} {displayName} {user?.email} {roleLabel && ( )} {/* Profile fields */} Личные данные setFirstName(e.target.value)} onBlur={(e) => handleProfileBlur('first_name', e.target.value.trim())} fullWidth /> setLastName(e.target.value)} onBlur={(e) => handleProfileBlur('last_name', e.target.value.trim())} fullWidth /> {user?.universal_code && ( { navigator.clipboard.writeText(user.universal_code); showSnack('Код скопирован'); }} > ), }} /> )} {/* City + Timezone */} Местоположение {settingsLoading ? ( ) : ( <> typeof opt === 'string' ? opt : `${opt.name}${opt.region ? `, ${opt.region}` : ''}` } inputValue={cityQuery} onInputChange={(_, val) => setCityQuery(val)} onChange={(_, opt) => { if (opt && typeof opt === 'object') { setCityQuery(opt.name || ''); const newSettings = { ...settings, preferences: { ...settings?.preferences, city: opt.name || '', timezone: opt.timezone || settings?.preferences?.timezone || '', }, }; scheduleSettingsSave(newSettings); } }} loading={citySearching} renderInput={(params) => ( { const newSettings = { ...settings, preferences: { ...settings?.preferences, city: cityQuery, }, }; scheduleSettingsSave(newSettings); }} InputProps={{ ...params.InputProps, endAdornment: ( <> {citySearching ? : null} {params.InputProps.endAdornment} ), }} /> )} renderOption={(props, opt) => ( {opt.name}{opt.region ? `, ${opt.region}` : ''} {opt.timezone && ( {opt.timezone} )} )} /> )} {/* Telegram */} Telegram {/* ─── RIGHT COLUMN ─── */} {/* Notification preferences */} Уведомления {notifSaving && } {settingsLoading ? ( ) : ( {/* Global toggle */} handleNotifChange({ enabled: !notifPrefs?.enabled })} /> } label="Включить уведомления" /> {notifPrefs?.enabled && ( <> {/* Channel toggles */} handleNotifChange({ email_enabled: !notifPrefs?.email_enabled })} /> } label="Email" /> handleNotifChange({ telegram_enabled: !notifPrefs?.telegram_enabled })} /> } label="Telegram" /> handleNotifChange({ in_app_enabled: !notifPrefs?.in_app_enabled })} /> } label="В приложении" /> Настройки по типу уведомлений )} )} {/* Per-child notification settings — parent only */} {user?.role === 'parent' && } {/* AI homework settings (mentor only) */} {user?.role === 'mentor' && settings && ( AI проверка домашних заданий {settingsSaving && } { const newVal = !settings?.mentor_homework_ai?.ai_trust_draft; scheduleSettingsSave({ ...settings, mentor_homework_ai: { ...settings?.mentor_homework_ai, ai_trust_draft: newVal, // Mutually exclusive ai_trust_publish: newVal ? false : settings?.mentor_homework_ai?.ai_trust_publish, }, }); }} /> } label="AI сохраняет как черновик (нужна ваша проверка)" /> { const newVal = !settings?.mentor_homework_ai?.ai_trust_publish; scheduleSettingsSave({ ...settings, mentor_homework_ai: { ...settings?.mentor_homework_ai, ai_trust_publish: newVal, // Mutually exclusive ai_trust_draft: newVal ? false : settings?.mentor_homework_ai?.ai_trust_draft, }, }); }} /> } label="AI публикует результат автоматически" /> Эти опции взаимно исключают друг друга )} setSnack((prev) => ({ ...prev, open: false }))} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} > setSnack((prev) => ({ ...prev, open: false }))}> {snack.message} ); }