"use client"; import { useState, useEffect } from "react"; import { Loader2, Plus, Check, ChevronRight, X, AlertCircle, BookOpen } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { parseChannelInput } from "@/lib/utils/channel-parser"; import { placementsApi, creativesApi, channelsApi } from "@/lib/api"; import type { Placement, Creative, Channel } from "@/lib/types/api"; import { Badge } from "./ui/badge"; interface ChannelInput { username: string; channelId: string; } interface CreatePlacementsDialogProps { workspaceId: string; projectId: string; existingPlacements: Placement[]; prefilledCreativeId?: string | null; open?: boolean; onOpenChange?: (open: boolean) => void; onSuccess: () => void; children?: React.ReactNode; } const DEFAULT_STATUS = "Согласование условий"; const DEFAULT_PLACEMENT_TYPE = "standard"; export function CreatePlacementsDialog({ workspaceId, projectId, existingPlacements, prefilledCreativeId = null, open: controlledOpen, onOpenChange, onSuccess, children, }: CreatePlacementsDialogProps) { const [internalOpen, setInternalOpen] = useState(false); const isOpen = controlledOpen !== undefined ? controlledOpen : internalOpen; const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [newChannelInput, setNewChannelInput] = useState(""); const [selectedChannels, setSelectedChannels] = useState([]); const [existingChannels, setExistingChannels] = useState([]); const [projectChannels, setProjectChannels] = useState([]); const [creativeId, setCreativeId] = useState(prefilledCreativeId); const [format, setFormat] = useState(""); const [cost, setCost] = useState(""); const [placementDate, setPlacementDate] = useState(""); const [comment, setComment] = useState(""); const [creatives, setCreatives] = useState([]); const [creating, setCreating] = useState(false); const [createdCount, setCreatedCount] = useState(null); const handleOpenChange = (isOpen: boolean) => { setInternalOpen(isOpen); onOpenChange?.(isOpen); }; const handleTriggerClick = () => { resetForm(); setInternalOpen(true); onOpenChange?.(true); }; const resetForm = () => { setStep(1); setNewChannelInput(""); setSelectedChannels([]); setExistingChannels([]); setProjectChannels([]); setCreativeId(prefilledCreativeId); setFormat(""); setCost(""); setPlacementDate(""); setComment(""); setError(null); setCreatedCount(null); }; const existingUsernames = new Set( existingPlacements .map((p) => p.channel.username?.toLowerCase()) .filter(Boolean) ); const getExistingChannelIdByUsername = (usernameLower: string): string | null => { const matched = existingPlacements.find( (p) => p.channel.username?.toLowerCase() === usernameLower ); return matched?.channel.id ?? null; }; useEffect(() => { if (isOpen) { loadCreatives(); loadProjectChannels(); } }, [isOpen, workspaceId, projectId, prefilledCreativeId]); const loadCreatives = async () => { try { const response = await creativesApi.list(workspaceId, { project_id: projectId, include_archived: false, size: 100, }); setCreatives(response.items); if (prefilledCreativeId && response.items.length > 0) { const prefilled = response.items.find((c) => c.id === prefilledCreativeId); if (prefilled) { setCreativeId(prefilled.id); } } } catch (err) { console.error("Failed to load creatives:", err); } }; const loadProjectChannels = async () => { try { const projectPlacements = await placementsApi.getByProject(workspaceId, projectId); const channels = projectPlacements.map((p) => p.channel); setProjectChannels(channels); } catch (err) { console.error("Failed to load project channels:", err); } }; const addChannel = async () => { const username = parseChannelInput(newChannelInput); if (!username) { setError("Не удалось распознать username"); return; } const lowerUsername = username.toLowerCase(); if (selectedChannels.some((c) => c.username.toLowerCase() === lowerUsername)) { setError("Канал уже добавлен в список"); return; } const projectChannel = projectChannels.find((c) => c.username?.toLowerCase() === lowerUsername); if (projectChannel) { setSelectedChannels([...selectedChannels, { username, channelId: projectChannel.id }]); setNewChannelInput(""); setError(null); return; } if (existingUsernames.has(lowerUsername)) { const existingChannelId = getExistingChannelIdByUsername(lowerUsername); if (!existingChannelId) { setError("Не удалось найти ID канала в проекте"); return; } setSelectedChannels([...selectedChannels, { username, channelId: existingChannelId }]); setNewChannelInput(""); setError(null); return; } try { const createResponse = await channelsApi.create([{ username }]); const channelId = createResponse.results[0]?.channel.id; if (!channelId) { setError("Не удалось получить ID канала"); return; } setSelectedChannels([...selectedChannels, { username, channelId }]); setNewChannelInput(""); setError(null); } catch (err) { console.error("Failed to create channel:", err); setError("Не удалось создать канал"); } }; const removeChannel = (username: string) => { setSelectedChannels(selectedChannels.filter((c) => c.username !== username)); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); addChannel(); } }; const canProceedStep1 = selectedChannels.length > 0; const handleCreate = async () => { try { setCreating(true); setError(null); const details: any = {}; details.placement_type = DEFAULT_PLACEMENT_TYPE; if (format) details.format = format; if (cost) details.cost = { type: "fixed", value: parseFloat(cost) }; if (placementDate) details.placement_at = new Date(placementDate).toISOString(); if (comment) details.comment = comment; await placementsApi.createBulk(workspaceId, projectId, { creative_id: creativeId || undefined, channels: selectedChannels.map((c) => ({ channel_id: c.channelId, status: DEFAULT_STATUS, })), details: Object.keys(details).length > 0 ? details : undefined, }); setCreatedCount(selectedChannels.length); onSuccess(); } catch (err: any) { console.error("Failed to create placements:", err); setError(err.detail || "Не удалось создать размещения"); setCreating(false); } }; const selectedCreative = creatives.find((c) => c.id === creativeId); const isControlled = controlledOpen !== undefined; return ( {children} Создать размещения Шаг {step} из 3:{" "} {step === 1 && "Выберите каналы"} {step === 2 && "Укажите параметры"} {step === 3 && "Подтвердите создание"} {createdCount !== null ? (

Размещения созданы!

Создано {createdCount} размещений

) : ( <>
{[1, 2, 3].map((s) => (
s ? "bg-emerald-500 text-white" : "bg-muted text-muted-foreground" )} > {step > s ? : s}
))}
{error && (
{error}
)} {step === 1 && (
setNewChannelInput(e.target.value)} onKeyDown={handleKeyDown} disabled={loading} />

Поддерживаются: t.me, telemetr.me, tgstat.ru, username с @

{selectedChannels.length > 0 && (
{selectedChannels.map((channel) => (
@{channel.username}
))}
)} {projectChannels.length > 0 && (
{projectChannels.length}
{projectChannels.map((channel) => (
{channel.title} {channel.username && ( @{channel.username} )}
{selectedChannels.some((c) => c.channelId === channel.id) ? ( ) : ( )}
))}
)}
)} {step === 2 && (
setFormat(e.target.value)} />
setCost(e.target.value)} />
setPlacementDate(e.target.value)} />