import React, { useState, useEffect } from 'react'; import { Send, MessageSquare, Share2, Zap, Sparkles, BarChart3, Layout, Plus, Hash, ImageIcon, Clock, CheckCircle2, Trash2, X, ChevronDown, ChevronUp, Lightbulb, UserPlus, ArrowRight, CheckCircle, XCircle, Edit, Eye, Mail, Calendar, FileText, Pencil } from 'lucide-react'; import { backendApi } from '../../services/apiClient'; import type { SMMChannel as SMMChannelType, AttractionAction, ScheduledPost, PREvent } from '../../types'; import { PublicationSchedule } from './PublicationSchedule'; import { EVENT_SMM_POST_FROM_EVENT } from '../../constants/refreshEvents'; const ACTION_TYPE_LABELS: Record = { mailing: 'Рассылка', event: 'Мероприятие', post: 'Пост', other: 'Другое', }; const ACTION_TYPE_ICONS: Record = { mailing: Mail, event: Calendar, post: FileText, other: Hash, }; const CHANNEL_STYLE: Record = { tg: { color: 'text-sky-500', bg: 'bg-sky-50', Icon: Send }, vk: { color: 'text-blue-600', bg: 'bg-blue-50', Icon: Share2 }, wa: { color: 'text-emerald-500', bg: 'bg-emerald-50', Icon: MessageSquare }, other: { color: 'text-slate-600', bg: 'bg-slate-50', Icon: Hash }, }; const UK_RECOMMENDATIONS = [ 'Информировать жителей о проделанной работе — отчёты, фото, сроки.', 'Вести диалог и обратную связь в комментариях, не оставлять вопросы без ответа.', 'Полезный контент: видео, истории жильцов, микрогиды по районам и дворам.', 'Единый стиль и оформление сообщества — узнаваемый бренд УК.', 'Таргетированная реклама при необходимости для привлечения в группу.', ]; const CONTENT_IDEAS = [ { tag: 'ЖКХ Хак', title: 'Как снизить платежи за отопление: 3 совета', desc: 'Статистика показывает, что этот пост соберет +45% охвата в холодное время.' }, { tag: 'Доверие', title: 'Знакомство с мастером: Сантехник Иван', desc: 'Персонализация снижает агрессию в чатах на 20%.' }, { tag: 'Отчёт', title: 'Отчёт о работе за месяц', desc: 'Кратко: что сделали по дому, куда ушли средства. Повышает доверие.' }, { tag: 'Микрогид', title: 'Микрогид по двору', desc: 'Где контейнеры, где парковка, контакты УК — удобно для новосёлов.' }, { tag: 'Реакция', title: 'Как мы отреагировали на заявку', desc: 'История: заявка → выезд → результат. Показывает работу УК.' }, ]; interface SMMManagerProps { onNavigateToAttraction?: () => void; } export const SMMManager: React.FC = ({ onNavigateToAttraction }) => { const [isGenerating, setIsGenerating] = useState(false); const [channels, setChannels] = useState([]); const [channelsLoading, setChannelsLoading] = useState(true); const [attractionList, setAttractionList] = useState([]); const [attractionLoading, setAttractionLoading] = useState(false); const [attractionModal, setAttractionModal] = useState(false); const [attractionEditing, setAttractionEditing] = useState(null); const [attractionForm, setAttractionForm] = useState({ title: '', description: '', channelId: '' as number | '', actionType: 'mailing' as AttractionAction['actionType'], actionDate: new Date().toISOString().slice(0, 10), newSubscribersAttributed: '' as number | '', eventId: '' as number | '', }); const [prEvents, setPrEvents] = useState>([]); const [attractionSaving, setAttractionSaving] = useState(false); const [recommendationsOpen, setRecommendationsOpen] = useState(false); const [scheduledPosts, setScheduledPosts] = useState([]); const [scheduledPostsLoading, setScheduledPostsLoading] = useState(false); const [viewPostModal, setViewPostModal] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [editContent, setEditContent] = useState(''); const [postActionLoading, setPostActionLoading] = useState(false); const [createPostModal, setCreatePostModal] = useState(false); const [postForm, setPostForm] = useState({ title: '', content: '', channelIds: [] as number[], scheduledAt: '', topicId: '' as number | '' }); const [postFormSaving, setPostFormSaving] = useState(false); const [approvedTopics, setApprovedTopics] = useState>([]); const [snapshotModal, setSnapshotModal] = useState<{ channel: SMMChannelType } | null>(null); const [snapshotCount, setSnapshotCount] = useState(''); const [snapshotNote, setSnapshotNote] = useState(''); const [snapshotSaving, setSnapshotSaving] = useState(false); const [addChannelModal, setAddChannelModal] = useState(false); const [newChannelName, setNewChannelName] = useState(''); const [newChannelType, setNewChannelType] = useState<'tg' | 'vk' | 'wa' | 'other'>('tg'); const [newChannelSaving, setNewChannelSaving] = useState(false); const loadChannels = () => { setChannelsLoading(true); backendApi.getSMMChannels() .then((list) => { setChannels(Array.isArray(list) ? list : []); }) .catch(() => setChannels([])) .finally(() => setChannelsLoading(false)); }; useEffect(() => { loadChannels(); }, []); const loadAttractionList = () => { setAttractionLoading(true); backendApi.getAttractionActions({ limit: 100 }) .then((data) => setAttractionList(Array.isArray(data) ? data : [])) .catch(() => setAttractionList([])) .finally(() => setAttractionLoading(false)); }; useEffect(() => { loadAttractionList(); }, []); const handleOpenAttractionCreate = () => { setAttractionEditing(null); setAttractionForm({ title: '', description: '', channelId: '', actionType: 'mailing', actionDate: new Date().toISOString().slice(0, 10), newSubscribersAttributed: '', eventId: '', }); setAttractionModal(true); }; const handleOpenAttractionEdit = (item: AttractionAction) => { setAttractionEditing(item); setAttractionForm({ title: item.title, description: item.description ?? '', channelId: item.channelId ?? '', actionType: item.actionType, actionDate: item.actionDate ? item.actionDate.slice(0, 10) : new Date().toISOString().slice(0, 10), newSubscribersAttributed: item.newSubscribersAttributed ?? '', eventId: item.eventId ?? '', }); setAttractionModal(true); }; useEffect(() => { if (attractionModal) { backendApi.getPREvents({ limit: 100 }) .then((list) => setPrEvents(Array.isArray(list) ? list.map(e => ({ id: Number(e.id), title: e.title, date: String(e.date) })) : [])) .catch(() => setPrEvents([])); } }, [attractionModal]); const handleSaveAttraction = () => { const title = attractionForm.title.trim(); if (!title) return; setAttractionSaving(true); const payload = { title, description: attractionForm.description.trim() || undefined, channelId: attractionForm.channelId === '' ? undefined : Number(attractionForm.channelId), actionType: attractionForm.actionType, actionDate: attractionForm.actionDate, newSubscribersAttributed: attractionForm.newSubscribersAttributed === '' ? undefined : Number(attractionForm.newSubscribersAttributed), eventId: attractionForm.actionType === 'event' && attractionForm.eventId !== '' ? Number(attractionForm.eventId) : undefined, }; if (attractionEditing) { backendApi.updateAttractionAction(attractionEditing.id, payload) .then(() => { setAttractionModal(false); loadAttractionList(); }) .finally(() => setAttractionSaving(false)); } else { backendApi.createAttractionAction(payload) .then(() => { setAttractionModal(false); loadAttractionList(); }) .finally(() => setAttractionSaving(false)); } }; const handleDeleteAttraction = (id: number) => { if (!confirm('Удалить запись о привлечении?')) return; backendApi.deleteAttractionAction(id).then(() => loadAttractionList()); }; const loadScheduledPosts = () => { setScheduledPostsLoading(true); backendApi.getScheduledPosts({ limit: 10 }) .then((data) => setScheduledPosts(Array.isArray(data) ? data : [])) .catch(() => setScheduledPosts([])) .finally(() => setScheduledPostsLoading(false)); }; useEffect(() => { loadScheduledPosts(); }, []); useEffect(() => { backendApi.getPostTopics({ status: 'approved', limit: 50 }) .then((data) => { const topics = Array.isArray(data) ? data : []; setApprovedTopics(topics.map(t => ({ id: t.id, title: t.title, scheduledDate: t.scheduledDate || `${t.month}-01` }))); }) .catch(() => setApprovedTopics([])); }, []); const handleOpenCreatePost = (topic?: { id: number; title: string; scheduledDate: string }) => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(10, 0, 0, 0); const scheduledDate = topic ? new Date(topic.scheduledDate) : tomorrow; scheduledDate.setHours(10, 0, 0, 0); setPostForm({ title: topic ? topic.title : '', content: '', channelIds: [], scheduledAt: scheduledDate.toISOString().slice(0, 16), topicId: topic ? topic.id : '' }); setCreatePostModal(true); }; useEffect(() => { const handler = (e: CustomEvent<{ event: PREvent }>) => { const ev = e.detail?.event; if (!ev) return; const eventDate = new Date(String(ev.date)); const publishDate = new Date(eventDate); publishDate.setDate(publishDate.getDate() - 1); publishDate.setHours(10, 0, 0, 0); const content = ev.announcement?.trim() || `Приглашаем на мероприятие «${ev.title}» — ${eventDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' })}. ${ev.location ? `Место: ${ev.location}.` : ''}`; setPostForm({ title: ev.title, content, channelIds: [], scheduledAt: publishDate.toISOString().slice(0, 16), topicId: '' }); setCreatePostModal(true); }; window.addEventListener(EVENT_SMM_POST_FROM_EVENT, handler as EventListener); return () => window.removeEventListener(EVENT_SMM_POST_FROM_EVENT, handler as EventListener); }, []); const handleSavePost = () => { if (!postForm.title.trim() || !postForm.content.trim() || !postForm.scheduledAt) return; setPostFormSaving(true); backendApi.createScheduledPost({ title: postForm.title.trim(), content: postForm.content.trim(), channelIds: postForm.channelIds, scheduledAt: new Date(postForm.scheduledAt).toISOString(), status: 'pending_approval', topicId: postForm.topicId === '' ? undefined : Number(postForm.topicId) }) .then(() => { setCreatePostModal(false); loadScheduledPosts(); }) .finally(() => setPostFormSaving(false)); }; const handleViewPost = (post: ScheduledPost) => { setViewPostModal(post); setRejectReason(''); setEditContent(post.content); }; const handleApprovePost = (id: number) => { if (!confirm('Одобрить пост для публикации?')) return; setPostActionLoading(true); backendApi.approveScheduledPost(id) .then((updated) => { if (updated && viewPostModal) { setViewPostModal({ ...viewPostModal, ...updated }); } loadScheduledPosts(); }) .finally(() => setPostActionLoading(false)); }; const handleRejectPost = (id: number) => { if (!rejectReason.trim()) { alert('Укажите причину отклонения'); return; } setPostActionLoading(true); backendApi.rejectScheduledPost(id, { rejectionReason: rejectReason }) .then(() => { setViewPostModal(null); setRejectReason(''); loadScheduledPosts(); }) .finally(() => setPostActionLoading(false)); }; const handleSendToEdit = (id: number) => { if (!editContent.trim()) { alert('Введите отредактированный текст'); return; } setPostActionLoading(true); backendApi.sendScheduledPostToEdit(id, { editedContent: editContent }) .then((updated) => { if (updated && viewPostModal) { setViewPostModal({ ...viewPostModal, ...updated }); } setEditContent(''); loadScheduledPosts(); }) .finally(() => setPostActionLoading(false)); }; const handleOpenSnapshot = (channel: SMMChannelType) => { setSnapshotModal({ channel }); setSnapshotCount(String(channel.subscribersCount ?? channel.lastSnapshot?.subscribersCount ?? '')); setSnapshotNote(''); }; const handleSaveSnapshot = () => { if (!snapshotModal) return; const count = parseInt(snapshotCount, 10); if (Number.isNaN(count) || count < 0) return; setSnapshotSaving(true); backendApi.createSMMChannelSnapshot(snapshotModal.channel.id, { subscribersCount: count, note: snapshotNote.trim() || undefined, }) .then(() => { setSnapshotModal(null); loadChannels(); }) .finally(() => setSnapshotSaving(false)); }; const handleAddChannel = () => { const name = newChannelName.trim(); if (!name) return; setNewChannelSaving(true); backendApi.createSMMChannel({ name, type: newChannelType }) .then(() => { setAddChannelModal(false); setNewChannelName(''); setNewChannelType('tg'); loadChannels(); }) .finally(() => setNewChannelSaving(false)); }; const handleDeleteChannel = (id: number) => { if (!confirm('Удалить канал? Снимки подписчиков тоже будут удалены.')) return; backendApi.deleteSMMChannel(id).then(() => loadChannels()); }; const getStatusColor = (status: ScheduledPost['status']) => { switch (status) { case 'approved': return 'text-emerald-600 bg-emerald-50'; case 'pending_approval': return 'text-amber-600 bg-amber-50'; case 'rejected': return 'text-red-600 bg-red-50'; case 'edited': return 'text-blue-600 bg-blue-50'; case 'published': return 'text-slate-600 bg-slate-50'; default: return 'text-slate-500 bg-slate-50'; } }; const getStatusLabel = (status: ScheduledPost['status']) => { switch (status) { case 'draft': return 'Черновик'; case 'pending_approval': return 'На согласовании'; case 'approved': return 'Одобрено'; case 'rejected': return 'Отклонено'; case 'edited': return 'На редактировании'; case 'published': return 'Опубликовано'; default: return status; } }; return (
{/* AI Post Generator */}

AI Контент-мейкер

Генерация постов из рабочих данных