'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 */}
>
);
}
// ----------------------------------------------------------------------
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}
);
}