"use client"; import { useState, useEffect, useRef } from "react"; import { Loader2, Plus, Check, ChevronRight, X, AlertCircle } 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"; interface ChannelInput { username: string; channelId: string; } interface CreatePlacementsDialogProps { workspaceId: string; projectId: string; existingPlacements: Placement[]; onSuccess: () => void; children?: React.ReactNode; } const DEFAULT_STATUS = "Согласование условий"; const DEFAULT_PLACEMENT_TYPE = "standard"; export function CreatePlacementsDialog({ workspaceId, projectId, existingPlacements, onSuccess, children, }: CreatePlacementsDialogProps) { const [open, setOpen] = useState(false); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Step 1: Channels const [newChannelInput, setNewChannelInput] = useState(""); const [selectedChannels, setSelectedChannels] = useState([]); const [existingChannels, setExistingChannels] = useState([]); // Step 2: Details const [creativeId, setCreativeId] = useState(null); const [format, setFormat] = useState(""); const [cost, setCost] = useState(""); const [placementDate, setPlacementDate] = useState(""); const [comment, setComment] = useState(""); // Step 3: Confirm const [creatives, setCreatives] = useState([]); const [creating, setCreating] = useState(false); const [createdCount, setCreatedCount] = useState(null); const existingUsernames = new Set( existingPlacements .map((p) => p.channel.username?.toLowerCase()) .filter(Boolean) ); const handleOpenChange = (isOpen: boolean) => { if (!isOpen) { resetForm(); } setOpen(isOpen); }; const resetForm = () => { setStep(1); setNewChannelInput(""); setSelectedChannels([]); setExistingChannels([]); setCreativeId(null); setFormat(""); setCost(""); setPlacementDate(""); setComment(""); setError(null); setCreatedCount(null); }; const loadCreatives = async () => { try { const response = await creativesApi.listByProject(workspaceId, projectId); setCreatives(response.items); } catch (err) { console.error("Failed to load creatives:", err); } }; const addChannel = async () => { const username = parseChannelInput(newChannelInput); if (!username) { setError("Не удалось распознать username"); return; } const lowerUsername = username.toLowerCase(); if (existingUsernames.has(lowerUsername)) { setError("Этот канал уже добавлен в проект"); return; } if (selectedChannels.some((c) => c.username.toLowerCase() === lowerUsername)) { setError("Канал уже добавлен в список"); return; } // Look up channel by username to get channel_id try { const channelsResponse = await channelsApi.list({ username }); const channel = channelsResponse.items.find( (c) => c.username?.toLowerCase() === lowerUsername ); if (!channel) { setError("Канал не найден в каталоге"); return; } setSelectedChannels([...selectedChannels, { username, channelId: channel.id }]); setNewChannelInput(""); setError(null); } catch (err) { console.error("Failed to look up 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 handleTriggerClick = () => { resetForm(); setOpen(true); }; const selectedCreative = creatives.find((c) => c.id === creativeId); return ( {children} Создать размещения Шаг {step} из 3:{" "} {step === 1 && "Выберите каналы"} {step === 2 && "Укажите параметры"} {step === 3 && "Подтвердите создание"} {createdCount !== null ? (

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

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

) : ( <> {/* Progress Steps */}
{[1, 2, 3].map((s) => (
s ? "bg-emerald-500 text-white" : "bg-muted text-muted-foreground" )} > {step > s ? : s}
))}
{/* Error */} {error && (
{error}
)} {/* Step 1: Channels */} {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}
))}
)}
)} {/* Step 2: Details */} {step === 2 && (
setFormat(e.target.value)} />
setCost(e.target.value)} />
setPlacementDate(e.target.value)} />