import React, { useState, useEffect, useContext, useCallback } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import {
Typography,
Box,
Grid,
Button,
ButtonGroup,
CircularProgress,
Alert,
Tabs,
Tab,
Chip,
Select,
MenuItem,
FormControl,
InputLabel,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
TextField,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Badge,
Tooltip,
IconButton,
Avatar,
AvatarGroup,
} from '@mui/material';
import {
Add as AddIcon,
Telegram as TelegramIcon,
CalendarMonth as CalendarViewIcon,
ViewList as ListViewIcon,
Edit as EditIcon,
} from '@mui/icons-material';
import Layout from '../components/Layout';
import PostCard from '../components/PostCard';
import PostModal from '../components/PostModal';
import PostFormModal from '../components/PostFormModal';
import { CalendarContext } from '../context/CalendarContext';
import { AuthContext } from '../context/AuthContext';
import { SocketContext } from '../context/SocketContext';
import { postAPI } from '../utils/api';
import { format, startOfMonth, endOfMonth, eachDayOfInterval,
getDay, isSameDay, isToday, startOfWeek, getWeek } from 'date-fns';
import { ru } from 'date-fns/locale';
import axios from 'axios';
const ViewMode = {
LIST: 'list',
CALENDAR: 'calendar'
};
const CalendarPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const location = useLocation();
const { user } = useContext(AuthContext);
const {
currentCalendar,
fetchCalendar,
loading: calendarLoading,
error: calendarError,
} = useContext(CalendarContext);
const {
joinCalendar,
activeUsers,
emitPostCreated,
emitPostUpdated,
emitPostDeleted,
socket
} = useContext(SocketContext);
// Локальное состояние
const [posts, setPosts] = useState([]);
const [filteredPosts, setFilteredPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState(0);
const [statusFilter, setStatusFilter] = useState('все');
const [typeFilter, setTypeFilter] = useState('все');
const [viewMode, setViewMode] = useState(ViewMode.CALENDAR);
const [currentDate, setCurrentDate] = useState(new Date());
// Добавляем состояние для модальных окон
const [selectedPostId, setSelectedPostId] = useState(null);
const [postModalOpen, setPostModalOpen] = useState(false);
const [formModalOpen, setFormModalOpen] = useState(false);
const [editingPostId, setEditingPostId] = useState(null);
const [selectedDate, setSelectedDate] = useState(null);
// Фильтрация публикаций - объявляем функцию до всех useEffect, которые её используют
const filterPosts = useCallback((postsToFilter = posts) => {
let filtered = [...postsToFilter];
// Фильтрация по вкладке (дате)
const today = new Date();
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
if (activeTab === 1) { // Прошлые
filtered = filtered.filter(post => new Date(post.publishDate) < today);
} else if (activeTab === 2) { // Будущие
filtered = filtered.filter(post => new Date(post.publishDate) > today);
} else if (activeTab === 3) { // Этот месяц
filtered = filtered.filter(post =>
new Date(post.publishDate) >= monthStart &&
new Date(post.publishDate) <= monthEnd
);
}
// Фильтрация по статусу
if (statusFilter !== 'все') {
filtered = filtered.filter(post => post.status === statusFilter);
}
// Фильтрация по типу
if (typeFilter !== 'все') {
filtered = filtered.filter(post => post.type === typeFilter);
}
// Сортировка по дате публикации
filtered.sort((a, b) => new Date(a.publishDate) - new Date(b.publishDate));
setFilteredPosts(filtered);
}, [posts, activeTab, statusFilter, typeFilter, currentDate]);
// Применение фильтров при изменении dependecies filterposts
useEffect(() => {
filterPosts();
}, [filterPosts]);
// Загрузка календаря и публикаций при монтировании
useEffect(() => {
const loadData = async () => {
try {
console.log('Загрузка календаря с ID:', id);
// Проверяем, есть ли токен аутентификации
const token = localStorage.getItem('token');
if (!token) {
console.error('Отсутствует токен аутентификации');
navigate('/login');
return;
}
// Устанавливаем заголовок авторизации
axios.defaults.headers.common['x-auth-token'] = token;
const calendar = await fetchCalendar(id);
if (calendar) {
console.log('Календарь успешно загружен:', calendar);
console.log('Владелец календаря:', calendar.owner);
console.log('Текущий пользователь:', user?._id);
await fetchPosts();
} else {
console.error('Календарь не удалось загрузить');
setError(calendarError || 'Не удалось загрузить календарь');
setLoading(false);
}
} catch (err) {
console.error('Ошибка при загрузке данных:', err);
setError('Произошла ошибка при загрузке данных');
setLoading(false);
}
};
// Если есть ID календаря и мы авторизованы, загружаем календарь
if (id && user) {
console.log('Пользователь авторизован, загружаем календарь', id);
loadData();
}
// Если ID календаря отсутствует, перенаправляем на главную
else if (!id) {
console.log('ID календаря отсутствует, перенаправление на главную');
navigate('/');
}
// Если нет пользователя, но есть ID, ждем загрузки пользователя
// Это предотвратит ненужное перенаправление на главную страницу
// eslint-disable-next-line
}, [id, user?._id]);
// Эффект для присоединения к комнате календаря
// Выделен в отдельный useEffect для гарантии присоединения даже при обновлении страницы
useEffect(() => {
if (id && user && socket && currentCalendar) {
console.log('Присоединяемся к комнате календаря', id);
joinCalendar(id);
}
}, [id, user, socket, currentCalendar, joinCalendar]);
// Настройка слушателей событий Socket.IO
useEffect(() => {
if (!socket) return;
// Обработчик нового поста
const handlePostCreated = ({ post }) => {
console.log('Получено событие о новом посте:', post._id);
setPosts(prevPosts => {
// Проверяем, нет ли уже этого поста
const exists = prevPosts.some(p => p._id === post._id);
if (exists) return prevPosts;
console.log('Добавляем новый пост в состояние:', post);
// Создаем новый массив с добавленным постом
const updatedPosts = [...prevPosts, post];
// Применяем фильтрацию сразу после обновления
filterPosts(updatedPosts);
return updatedPosts;
});
};
// Обработчик обновления поста
const handlePostUpdated = ({ post }) => {
console.log('Получено событие об обновлении поста:', post._id);
setPosts(prevPosts => {
const updated = prevPosts.map(p =>
p._id === post._id ? post : p
);
// Применяем фильтрацию сразу после обновления
filterPosts(updated);
return updated;
});
};
// Обработчик удаления поста
const handlePostDeleted = ({ postId }) => {
console.log('Получено событие об удалении поста:', postId);
setPosts(prevPosts => {
const filtered = prevPosts.filter(p => p._id !== postId);
// Применяем фильтрацию сразу после обновления
filterPosts(filtered);
return filtered;
});
// Если пост открыт в модальном окне, закрываем его
if (selectedPostId === postId) {
console.log('Закрываем модальное окно, т.к. публикация была удалена');
setPostModalOpen(false);
setSelectedPostId(null);
}
};
// Добавляем слушателей
socket.on('post_created', handlePostCreated);
socket.on('post_updated', handlePostUpdated);
socket.on('post_deleted', handlePostDeleted);
// Удаляем слушателей при размонтировании
return () => {
socket.off('post_created', handlePostCreated);
socket.off('post_updated', handlePostUpdated);
socket.off('post_deleted', handlePostDeleted);
};
}, [socket, filterPosts]);
// Получение всех публикаций для календаря
const fetchPosts = async () => {
setLoading(true);
try {
console.log('Загрузка публикаций для календаря:', id);
const response = await postAPI.getPosts(id);
if (response.success) {
console.log('Публикации загружены:', response.data.length);
setPosts(response.data);
// После обновления публикаций применяем фильтры
filterPosts(response.data);
setError(null);
} else {
console.error('Ошибка при загрузке публикаций:', response.error);
setError(response.error || 'Ошибка при загрузке публикаций');
}
} catch (err) {
console.error('Исключение при загрузке публикаций:', err);
setError('Ошибка при загрузке публикаций. Проверьте сетевое соединение.');
} finally {
setLoading(false);
}
};
// Обработчики действий с публикациями
const handleDeletePost = async (postId) => {
try {
console.log(`Удаление публикации с ID: ${postId}`);
const response = await postAPI.deletePost(postId);
if (response.success) {
console.log('Публикация успешно удалена');
// Обновляем локальное состояние
setPosts(posts.filter(post => post._id !== postId));
// Уведомляем других пользователей через Socket.IO
emitPostDeleted(postId);
} else {
console.error('Ошибка при удалении публикации:', response.error);
// Если публикация не найдена (404), всё равно обновляем интерфейс
if (response.statusCode === 404) {
console.log('Публикация не найдена на сервере, обновляем UI');
setPosts(posts.filter(post => post._id !== postId));
emitPostDeleted(postId);
} else {
// Для других ошибок показываем сообщение
setError(`Ошибка при удалении публикации: ${response.error}`);
}
}
} catch (err) {
console.error('Исключение при удалении публикации:', err);
setError('Ошибка при удалении публикации: ' + (err.message || ''));
}
};
const handleSendToTelegram = async (postId) => {
try {
const response = await postAPI.sendPostToTelegram(postId);
if (response.success) {
// Обновляем статус публикации
const updatedPosts = posts.map((post) =>
post._id === postId ? { ...post, status: 'готово к проверке' } : post
);
setPosts(updatedPosts);
// Обновляем отфильтрованные публикации
filterPosts(updatedPosts);
alert('Публикация успешно отправлена в Telegram');
// Если пост открыт в модальном окне, обновляем его статус
if (selectedPostId === postId) {
setPostModalOpen(false);
setTimeout(() => {
setPostModalOpen(true);
}, 100);
}
} else {
setError(response.error);
}
} catch (err) {
setError('Ошибка при отправке публикации в Telegram');
}
};
// Обработчики для модального окна просмотра поста
const handleOpenPostModal = (postId) => {
setSelectedPostId(postId);
setPostModalOpen(true);
};
const handleClosePostModal = () => {
setPostModalOpen(false);
};
// Обработчики для модального окна создания/редактирования поста
const handleCreatePost = (date) => {
setEditingPostId(null);
setSelectedDate(date);
setFormModalOpen(true);
};
const handleEditPost = (postId) => {
setEditingPostId(postId);
setFormModalOpen(true);
};
const handleCloseFormModal = () => {
setFormModalOpen(false);
setEditingPostId(null);
setSelectedDate(null);
};
const handlePostSaved = (post, calendarId) => {
// Обновляем список постов после сохранения
if (post) {
if (editingPostId) {
// Пост был обновлен
console.log('Пост обновлен:', post._id);
// Обновляем локальное состояние
setPosts(prevPosts =>
prevPosts.map(p => p._id === post._id ? post : p)
);
// Уведомляем других пользователей через Socket.IO
emitPostUpdated(post);
} else {
// Был создан новый пост
console.log('Создан новый пост:', post._id);
// Обновляем локальное состояние
setPosts(prevPosts => [...prevPosts, post]);
// Уведомляем других пользователей через Socket.IO
emitPostCreated(post);
}
} else {
// На всякий случай обновляем полный список постов с сервера
fetchPosts();
}
// Сбрасываем идентификатор редактируемого поста
setEditingPostId(null);
setFormModalOpen(false);
};
// Функции для отображения календаря
const getCalendarDays = () => {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const startDate = startOfWeek(monthStart, { locale: ru });
// Получаем все дни месяца
return eachDayOfInterval({ start: startDate, end: monthEnd });
};
const getPostsForDay = (day) => {
return filteredPosts.filter(post =>
isSameDay(new Date(post.publishDate), day)
);
};
const renderCalendarDay = (day) => {
const postsForDay = getPostsForDay(day);
const isCurrentMonth = day.getMonth() === currentDate.getMonth();
const isCurrentDay = isToday(day);
return (
{format(day, 'd')}
{postsForDay.length > 0 && (
)}
{postsForDay.slice(0, 3).map(post => (
handleOpenPostModal(post._id)}
sx={{
mb: 0.5,
p: 0.5,
bgcolor: post.type === 'пост' ? 'rgba(33, 150, 243, 0.1)' :
post.type === 'reels' ? 'rgba(244, 67, 54, 0.1)' :
post.type === 'рассылка' ? 'rgba(76, 175, 80, 0.1)' :
post.type === 'история' ? 'rgba(255, 152, 0, 0.1)' : 'grey.200',
borderRadius: 1,
fontSize: '0.75rem',
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
'&:hover': {
opacity: 0.8,
}
}}
>
{post.title}
))}
{postsForDay.length > 3 && (
+{postsForDay.length - 3} ещё
)}
{isCurrentMonth && (
handleCreatePost(day)}
sx={{ ml: 'auto', display: 'block', padding: '2px' }}
>
)}
);
};
const renderCalendarHeader = () => {
const weekDays = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
return (
{weekDays.map((day, index) => (
= 5 ? 'error.main' : 'text.primary'}
>
{day}
))}
);
};
const renderCalendarGrid = () => {
const days = getCalendarDays();
return (
{days.map((day) => (
{renderCalendarDay(day)}
))}
);
};
const renderCalendarNavigation = () => {
// Проверяем, является ли текущий пользователь владельцем календаря
const isOwner = currentCalendar && user && currentCalendar.owner._id === user._id;
return (
{format(currentDate, 'LLLL yyyy', { locale: ru })}
{/* Отображаем активных пользователей */}
{activeUsers.length > 0 && (
Сейчас в календаре:
{activeUsers.map(user => (
{user.name}
))}
{/* Правая часть - управление */}
);
};
// Фильтрация постов по текущему месяцу для режима списка
const getPostsForCurrentMonth = () => {
if (viewMode === ViewMode.LIST) {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
return filteredPosts.filter(post => {
const postDate = new Date(post.publishDate);
return postDate >= monthStart && postDate <= monthEnd;
});
}
return filteredPosts;
};
// Получаем посты для текущего месяца в режиме списка
const currentMonthPosts = getPostsForCurrentMonth();
if (calendarLoading || (loading && posts.length === 0)) {
return (
);
}
// Форматируем дату последнего обновления
const formattedUpdatedAt = format(
new Date(currentCalendar.updatedAt),
"d MMMM yyyy 'в' HH:mm",
{ locale: ru }
);
return (