"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { Loader2, ArrowLeft, ArrowRight, Check, Calendar } from "lucide-react"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; 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 { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Calendar as CalendarComponent, } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { placementsApi, creativesApi, channelsApi } from "@/lib/api"; import { isPermissionError, showSuccessToast, showErrorToast } from "@/lib/utils/error-handler"; import { ChannelInput, type ParsedChannel } from "@/components/channel-input"; import type { Creative } from "@/lib/types/api"; interface PlacementWizardProps { open: boolean; onOpenChange: (open: boolean) => void; workspaceId: string; projectId: string; initialCreativeId?: string | null; onSuccess?: () => void; } export function PlacementWizard({ open, onOpenChange, workspaceId, projectId, initialCreativeId, onSuccess, }: PlacementWizardProps) { const router = useRouter(); const [step, setStep] = useState(1); const [creatives, setCreatives] = useState([]); const [loading, setLoading] = useState(true); // Step 1: Creative const [selectedCreative, setSelectedCreative] = useState(null); // Step 2: Channels (parsed from input) const [parsedChannels, setParsedChannels] = useState([]); // Step 3: Details const [placementDate, setPlacementDate] = useState(new Date()); const [cost, setCost] = useState(""); const [comment, setComment] = useState(""); const [inviteLinkType, setInviteLinkType] = useState<"approval" | "public">("approval"); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (open) { loadData(); } else { // Reset on close setStep(1); setParsedChannels([]); setCost(""); setComment(""); } }, [open]); useEffect(() => { if (initialCreativeId && creatives.length > 0) { const creative = creatives.find(c => c.id === initialCreativeId); if (creative) { setSelectedCreative(creative); setStep(2); } } }, [initialCreativeId, creatives]); const loadData = async () => { try { setLoading(true); const response = await creativesApi.list(workspaceId, { project_id: projectId, size: 100 }); setCreatives(response.items); } catch (err) { console.error("Failed to load data:", err); } finally { setLoading(false); } }; const getValidChannelIds = () => { return parsedChannels .filter((c) => c.isValid && c.channel) .map((c) => c.channel!.id); }; const handleSubmit = async () => { if (!selectedCreative) return; const validChannels = parsedChannels.filter((c) => c.isValid); const existingChannelIds = validChannels.filter((c) => c.channel).map((c) => c.channel!.id); const newChannels = validChannels.filter((c) => !c.channel && c.normalized); try { setSubmitting(true); setError(null); // Create new channels first const newChannelIds: string[] = []; if (newChannels.length > 0) { const channelInputs = newChannels.map((c) => { if (c.type === "invite_link") { return { invite_link: `https://t.me/+${c.normalized}` }; } return { username: c.normalized! }; }); const createResult = await channelsApi.create(channelInputs); for (const result of createResult.results) { if ((result.status === "created" || result.status === "updated") && result.channel) { newChannelIds.push(result.channel.id); } } } const allChannelIds = [...existingChannelIds, ...newChannelIds]; if (allChannelIds.length === 0) { setError("Не выбрано ни одного канала"); setSubmitting(false); return; } // Create placements await placementsApi.createInProject(workspaceId, projectId, { creative_id: selectedCreative.id, channels: allChannelIds.map((channelId) => ({ channel_id: channelId, status: "Без статуса", comment: undefined, details: { placement_at: placementDate.toISOString(), cost: cost ? { type: "fixed" as const, value: parseFloat(cost) } : undefined, invite_link_type: inviteLinkType, }, })), }); showSuccessToast("Размещения созданы", "Успешно добавлено"); onOpenChange(false); onSuccess?.(); } catch (err: any) { if (isPermissionError(err)) { showErrorToast("Недостаточно прав", "Обратитесь к администратору"); } else { setError(err?.detail || "Ошибка создания размещения"); } } finally { setSubmitting(false); } }; const handleClose = () => { onOpenChange(false); router.push(`/dashboard/${workspaceId}/purchase-plans/${projectId}`); }; if (!open) return null; const validChannels = parsedChannels.filter((c) => c.isValid); const canProceedStep2 = validChannels.length > 0; return ( Создание размещения Шаг {step} из 3 {loading ? (
) : ( <> {/* Progress */}
{[1, 2, 3].map((s) => (
= s ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground" )} > {step > s ? : s}
{s < 3 && (
s ? "bg-primary" : "bg-muted" )} /> )}
))}
{/* Step 1: Creative */} {step === 1 && (

Выберите креатив

{creatives.filter(c => c.status === "active").map((creative) => ( ))}
)} {/* Step 2: Channels */} {step === 2 && (

Добавьте каналы

Введите каналы в любом формате, например:

t.me/username, @username, tgstat.ru/channel/@username, telemetr.me/content/username

)} {/* Step 3: Details */} {step === 3 && (

Детали размещения

{/* Summary of selected channels */}

Выбрано каналов: {validChannels.length}

{validChannels.slice(0, 5).map((c, i) => ( {c.normalized} ))} {validChannels.length > 5 && ( +{validChannels.length - 5} ещё )}
date && setPlacementDate(date)} locale={ru} disabled={(date) => date < new Date()} />
setCost(e.target.value)} />