Files
tgex-frontend/app/(dashboard)/external-channels/import/page.tsx
2025-11-10 12:07:24 +03:00

339 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// ============================================================================
// External Channels Import Page
// ============================================================================
import React, { useState } 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 { Badge } from "@/components/ui/badge";
import { externalChannelsApi } from "@/lib/api";
import {
FileUp,
AlertCircle,
CheckCircle,
Loader2,
Download,
ArrowLeft,
X,
} from "lucide-react";
import type { ExternalChannelImportResponse } from "@/lib/types/api";
export default function ImportExternalChannelsPage() {
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [result, setResult] = useState<ExternalChannelImportResponse | null>(
null
);
const [error, setError] = useState<string | null>(null);
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 = (selectedFile: File) => {
// Проверка типа файла
const validTypes = [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/csv",
];
if (
!validTypes.includes(selectedFile.type) &&
!selectedFile.name.match(/\.(xlsx|xls|csv)$/i)
) {
setError("Пожалуйста, выберите файл Excel (.xlsx, .xls) или CSV");
return;
}
setFile(selectedFile);
setError(null);
setResult(null);
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
handleFileSelect(selectedFile);
}
};
const handleUpload = async () => {
if (!file) return;
try {
setIsUploading(true);
setError(null);
// Для примера используем первый целевой канал (tc1)
// В реальном приложении нужно дать пользователю выбрать
const importResult = await externalChannelsApi.import(file, "tc1");
setResult(importResult);
setFile(null);
} catch (err: any) {
setError(err?.error?.message || "Ошибка импорта файла");
} finally {
setIsUploading(false);
}
};
const handleReset = () => {
setFile(null);
setResult(null);
setError(null);
};
const handleGoToChannels = () => {
router.push("/external-channels");
};
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Внешние каналы", href: "/external-channels" },
{ label: "Импорт" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4 max-w-4xl mx-auto w-full">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Импорт внешних каналов
</h1>
<p className="text-muted-foreground mt-1">
Загрузите файл Excel с данными из Tgstat
</p>
</div>
<Button asChild variant="outline">
<Link href="/external-channels">
<ArrowLeft className="mr-2 h-4 w-4" />
Назад
</Link>
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Формат файла</CardTitle>
<CardDescription>
Требования к структуре Excel файла
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 text-sm">
<p>Файл должен содержать следующие колонки:</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>
<strong>Название</strong> - название канала
</li>
<li>
<strong>Username</strong> - username канала (с @ или без)
</li>
<li>
<strong>Ссылка</strong> - ссылка на канал (t.me/...)
</li>
<li>
<strong>Подписчики</strong> - количество подписчиков (число)
</li>
<li>
<strong>Описание</strong> - краткое описание (опционально)
</li>
</ul>
<div className="pt-2">
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Скачать шаблон
</Button>
</div>
</div>
</CardContent>
</Card>
{!result ? (
<Card>
<CardHeader>
<CardTitle>Загрузка файла</CardTitle>
<CardDescription>
Перетащите файл или выберите его
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`
border-2 border-dashed rounded-lg p-12 text-center transition-colors
${
isDragging
? "border-primary bg-primary/5"
: "border-muted-foreground/25"
}
${file ? "bg-muted/50" : ""}
`}
>
{file ? (
<div className="space-y-4">
<div className="flex items-center justify-center gap-2">
<FileUp className="h-8 w-8 text-primary" />
</div>
<div>
<p className="font-medium">{file.name}</p>
<p className="text-sm text-muted-foreground">
{(file.size / 1024).toFixed(2)} KB
</p>
</div>
<Button variant="outline" size="sm" onClick={handleReset}>
<X className="mr-2 h-4 w-4" />
Выбрать другой файл
</Button>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-center">
<FileUp className="h-12 w-12 text-muted-foreground" />
</div>
<div>
<p className="text-lg font-medium">
Перетащите файл сюда
</p>
<p className="text-sm text-muted-foreground">
или нажмите кнопку ниже
</p>
</div>
<div>
<label htmlFor="file-upload">
<Button asChild variant="outline">
<span>
<FileUp className="mr-2 h-4 w-4" />
Выбрать файл
</span>
</Button>
</label>
<input
id="file-upload"
type="file"
accept=".xlsx,.xls,.csv"
onChange={handleFileInputChange}
className="hidden"
/>
</div>
</div>
)}
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{file && (
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={handleReset}>
Отмена
</Button>
<Button onClick={handleUpload} disabled={isUploading}>
{isUploading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Импорт...
</>
) : (
<>
<FileUp className="mr-2 h-4 w-4" />
Импортировать
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<CheckCircle className="h-6 w-6 text-green-500" />
<CardTitle>Импорт завершен</CardTitle>
</div>
<CardDescription>Результаты обработки файла</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<div className="flex items-center justify-between p-3 rounded-lg bg-green-50 dark:bg-green-950/20">
<span className="text-sm font-medium">
Импортировано каналов
</span>
<Badge variant="default">{result.imported}</Badge>
</div>
{result.skipped > 0 && (
<div className="flex items-center justify-between p-3 rounded-lg bg-yellow-50 dark:bg-yellow-950/20">
<span className="text-sm font-medium">Пропущено</span>
<Badge variant="secondary">{result.skipped}</Badge>
</div>
)}
</div>
{result.errors.length > 0 && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium mb-2">
Обнаружены ошибки при импорте:
</p>
<ul className="list-disc list-inside space-y-1 text-sm">
{result.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
<div className="flex justify-end gap-2 pt-4">
<Button variant="outline" onClick={handleReset}>
Импортировать еще
</Button>
<Button onClick={handleGoToChannels}>Перейти к каналам</Button>
</div>
</CardContent>
</Card>
)}
</div>
</>
);
}