"use client"; // ============================================================================ // External Channels Import Page // ============================================================================ import React, { useState, useEffect } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { externalChannelsApi, channelsApi } from "@/lib/api"; import { parseChannelsFile, downloadTemplate, type ParsedChannel, } from "@/lib/utils/file-parser"; import { FileUp, AlertCircle, CheckCircle, Loader2, Download, ArrowLeft, X, FileSpreadsheet, } from "lucide-react"; import type { TargetChannel } from "@/lib/types/api"; interface ImportResult { total: number; successful: number; failed: number; errors: string[]; } export default function ImportExternalChannelsPage() { const router = useRouter(); const [file, setFile] = useState(null); const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); const [parsedChannels, setParsedChannels] = useState([]); const [parseErrors, setParseErrors] = useState([]); const [importResult, setImportResult] = useState(null); const [progress, setProgress] = useState(0); const [currentChannel, setCurrentChannel] = useState(""); const [targetChannels, setTargetChannels] = useState([]); const [selectedTargetChannel, setSelectedTargetChannel] = useState(""); useEffect(() => { loadTargetChannels(); }, []); const loadTargetChannels = async () => { try { const response = await channelsApi.list(); setTargetChannels(response.target_channels); if (response.target_channels.length > 0) { setSelectedTargetChannel(response.target_channels[0].id); } } catch (err) { console.error("Error loading target channels:", err); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = () => { setIsDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const droppedFile = e.dataTransfer.files[0]; if (droppedFile) { handleFileSelect(droppedFile); } }; const handleFileSelect = async (selectedFile: File) => { if (!selectedFile.name.match(/\.(xlsx|xls|csv)$/i)) { setParseErrors(["Пожалуйста, выберите файл Excel (.xlsx, .xls) или CSV"]); return; } setFile(selectedFile); setParseErrors([]); setParsedChannels([]); setImportResult(null); // Парсим файл try { const result = await parseChannelsFile(selectedFile); setParsedChannels(result.channels); setParseErrors(result.errors); } catch (err) { setParseErrors([ `Ошибка парсинга файла: ${ err instanceof Error ? err.message : "неизвестная ошибка" }`, ]); } }; const handleFileInputChange = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (selectedFile) { handleFileSelect(selectedFile); } }; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const handleImport = async () => { if (!selectedTargetChannel || parsedChannels.length === 0) return; setIsUploading(true); setImportResult(null); setProgress(0); setCurrentChannel(""); const result: ImportResult = { total: parsedChannels.length, successful: 0, failed: 0, errors: [], }; for (let i = 0; i < parsedChannels.length; i++) { const channel = parsedChannels[i]; setCurrentChannel(channel.title); setProgress(((i + 1) / parsedChannels.length) * 100); try { await externalChannelsApi.create({ telegram_id: channel.telegram_id || 0, title: channel.title, username: channel.username || null, description: channel.description || null, subscribers_count: channel.subscribers_count || null, target_channel_ids: [selectedTargetChannel], }); result.successful++; } catch (err: any) { result.failed++; const errorMsg = err?.detail || err?.error?.message || "Неизвестная ошибка"; result.errors.push( `Строка ${channel.row} (${channel.title}): ${errorMsg}` ); } // Задержка между запросами (300ms) if (i < parsedChannels.length - 1) { await delay(300); } } setImportResult(result); setIsUploading(false); setCurrentChannel(""); }; const handleReset = () => { setFile(null); setParsedChannels([]); setParseErrors([]); setImportResult(null); setProgress(0); }; return ( <>

Импорт внешних каналов

Загрузите файл Excel или CSV с данными каналов

{/* Выбор целевого канала */} Целевой канал Выберите целевой канал для привязки импортируемых каналов {/* Шаблоны */} Скачать шаблон Используйте шаблон для правильного заполнения данных {/* Загрузка файла */} Загрузка файла Перетащите файл или нажмите для выбора
{/* Результаты парсинга */} {parsedChannels.length > 0 && !importResult && ( Результаты парсинга Найдено каналов: {parsedChannels.length} {parseErrors.length > 0 && ( Ошибки при парсинге:
    {parseErrors.slice(0, 5).map((error, i) => (
  • {error}
  • ))} {parseErrors.length > 5 && (
  • ...и еще {parseErrors.length - 5}
  • )}
)}
)} {/* Прогресс импорта */} {isUploading && ( Импорт в процессе {currentChannel && `Импортируется: ${currentChannel}`}

{Math.round(progress)}% завершено

)} {/* Результаты импорта */} {importResult && ( Результаты импорта
{importResult.total}
Всего
{importResult.successful}
Успешно
{importResult.failed}
Ошибок
{importResult.errors.length > 0 && ( Ошибки импорта:
    {importResult.errors.map((error, i) => (
  • {error}
  • ))}
)} {importResult.successful > 0 && ( Успешно импортировано каналов: {importResult.successful} )}
)} {/* Инструкция */} Формат файла Требования к структуре файла

Колонки (в порядке):

  1. Название канала* - обязательное поле
  2. Username - без символа @ (опционально)
  3. Telegram ID - числовой ID канала (опционально)
  4. Подписчиков - количество подписчиков (опционально)
  5. Описание - описание канала (опционально)

* Первая строка файла должна содержать заголовки и будет пропущена при импорте

); }