fix: creatre placement fix

This commit is contained in:
ivannoskov
2026-02-02 11:42:03 +03:00
parent 74ef1108e1
commit 066f6a4f5b

View File

@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -27,6 +26,7 @@ import { cn } from "@/lib/utils";
import { parseChannelInput } from "@/lib/utils/channel-parser"; import { parseChannelInput } from "@/lib/utils/channel-parser";
import { placementsApi, creativesApi, channelsApi } from "@/lib/api"; import { placementsApi, creativesApi, channelsApi } from "@/lib/api";
import type { Placement, Creative, Channel } from "@/lib/types/api"; import type { Placement, Creative, Channel } from "@/lib/types/api";
import { Badge } from "./ui/badge";
interface ChannelInput { interface ChannelInput {
username: string; username: string;
@@ -64,41 +64,20 @@ export function CreatePlacementsDialog({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Step 1: Channels
const [newChannelInput, setNewChannelInput] = useState(""); const [newChannelInput, setNewChannelInput] = useState("");
const [selectedChannels, setSelectedChannels] = useState<ChannelInput[]>([]); const [selectedChannels, setSelectedChannels] = useState<ChannelInput[]>([]);
const [existingChannels, setExistingChannels] = useState<string[]>([]); const [existingChannels, setExistingChannels] = useState<string[]>([]);
const [projectChannels, setProjectChannels] = useState<Channel[]>([]); const [projectChannels, setProjectChannels] = useState<Channel[]>([]);
// Step 2: Details
const [creativeId, setCreativeId] = useState<string | null>(prefilledCreativeId); const [creativeId, setCreativeId] = useState<string | null>(prefilledCreativeId);
const [format, setFormat] = useState(""); const [format, setFormat] = useState("");
const [cost, setCost] = useState(""); const [cost, setCost] = useState("");
const [placementDate, setPlacementDate] = useState(""); const [placementDate, setPlacementDate] = useState("");
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
// Step 3: Confirm
const [creatives, setCreatives] = useState<Creative[]>([]); const [creatives, setCreatives] = useState<Creative[]>([]);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [createdCount, setCreatedCount] = useState<number | null>(null); const [createdCount, setCreatedCount] = useState<number | null>(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;
};
const handleOpenChange = (isOpen: boolean) => { const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
resetForm();
}
setInternalOpen(isOpen); setInternalOpen(isOpen);
onOpenChange?.(isOpen); onOpenChange?.(isOpen);
}; };
@@ -124,7 +103,26 @@ export function CreatePlacementsDialog({
setCreatedCount(null); setCreatedCount(null);
}; };
// Load creatives when dialog opens or when moving to step 2 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 () => { const loadCreatives = async () => {
try { try {
const response = await creativesApi.list(workspaceId, { const response = await creativesApi.list(workspaceId, {
@@ -134,7 +132,6 @@ export function CreatePlacementsDialog({
}); });
setCreatives(response.items); setCreatives(response.items);
// Auto-select prefilled creative if set
if (prefilledCreativeId && response.items.length > 0) { if (prefilledCreativeId && response.items.length > 0) {
const prefilled = response.items.find((c) => c.id === prefilledCreativeId); const prefilled = response.items.find((c) => c.id === prefilledCreativeId);
if (prefilled) { if (prefilled) {
@@ -146,7 +143,6 @@ export function CreatePlacementsDialog({
} }
}; };
// Load project channels when opening dialog
const loadProjectChannels = async () => { const loadProjectChannels = async () => {
try { try {
const projectPlacements = await placementsApi.getByProject(workspaceId, projectId); const projectPlacements = await placementsApi.getByProject(workspaceId, projectId);
@@ -157,14 +153,6 @@ export function CreatePlacementsDialog({
} }
}; };
// Load creatives and project channels when dialog opens
useEffect(() => {
if (isOpen) {
loadCreatives();
loadProjectChannels();
}
}, [isOpen, workspaceId, projectId, prefilledCreativeId]);
const addChannel = async () => { const addChannel = async () => {
const username = parseChannelInput(newChannelInput); const username = parseChannelInput(newChannelInput);
if (!username) { if (!username) {
@@ -178,7 +166,6 @@ export function CreatePlacementsDialog({
return; return;
} }
// Check if channel exists in project channels
const projectChannel = projectChannels.find((c) => c.username?.toLowerCase() === lowerUsername); const projectChannel = projectChannels.find((c) => c.username?.toLowerCase() === lowerUsername);
if (projectChannel) { if (projectChannel) {
setSelectedChannels([...selectedChannels, { username, channelId: projectChannel.id }]); setSelectedChannels([...selectedChannels, { username, channelId: projectChannel.id }]);
@@ -187,7 +174,6 @@ export function CreatePlacementsDialog({
return; return;
} }
// If channel already has placements in this project, reuse its channel_id.
if (existingUsernames.has(lowerUsername)) { if (existingUsernames.has(lowerUsername)) {
const existingChannelId = getExistingChannelIdByUsername(lowerUsername); const existingChannelId = getExistingChannelIdByUsername(lowerUsername);
if (!existingChannelId) { if (!existingChannelId) {
@@ -200,7 +186,6 @@ export function CreatePlacementsDialog({
return; return;
} }
// Channel doesn't exist in project yet. Create it globally via API to get channel_id.
try { try {
const createResponse = await channelsApi.create([{ username }]); const createResponse = await channelsApi.create([{ username }]);
const channelId = createResponse.results[0]?.channel.id; const channelId = createResponse.results[0]?.channel.id;
@@ -263,15 +248,13 @@ export function CreatePlacementsDialog({
}; };
const selectedCreative = creatives.find((c) => c.id === creativeId); const selectedCreative = creatives.find((c) => c.id === creativeId);
// If open is controlled, don't render DialogTrigger
const isControlled = controlledOpen !== undefined; const isControlled = controlledOpen !== undefined;
return ( return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}> <Dialog open={isOpen} onOpenChange={handleOpenChange}>
{!isControlled && ( <DialogTrigger asChild onClick={isControlled ? undefined : handleTriggerClick}>
<DialogTrigger asChild onClick={handleTriggerClick}>{children}</DialogTrigger> {children}
)} </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Создать размещения</DialogTitle> <DialogTitle>Создать размещения</DialogTitle>
@@ -297,7 +280,6 @@ export function CreatePlacementsDialog({
</div> </div>
) : ( ) : (
<> <>
{/* Progress Steps */}
<div className="flex items-center justify-center gap-2 mb-6"> <div className="flex items-center justify-center gap-2 mb-6">
{[1, 2, 3].map((s) => ( {[1, 2, 3].map((s) => (
<div <div
@@ -323,7 +305,6 @@ export function CreatePlacementsDialog({
))} ))}
</div> </div>
{/* Error */}
{error && ( {error && (
<div className="flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm mb-4"> <div className="flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm mb-4">
<AlertCircle className="h-4 w-4 shrink-0" /> <AlertCircle className="h-4 w-4 shrink-0" />
@@ -331,10 +312,8 @@ export function CreatePlacementsDialog({
</div> </div>
)} )}
{/* Step 1: Channels */}
{step === 1 && ( {step === 1 && (
<div className="space-y-4"> <div className="space-y-4">
{/* Add new channel */}
<div className="space-y-2"> <div className="space-y-2">
<Label>Добавить канал</Label> <Label>Добавить канал</Label>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -354,7 +333,6 @@ export function CreatePlacementsDialog({
</p> </p>
</div> </div>
{/* Selected channels */}
{selectedChannels.length > 0 && ( {selectedChannels.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<Label>Выбранные каналы ({selectedChannels.length})</Label> <Label>Выбранные каналы ({selectedChannels.length})</Label>
@@ -379,7 +357,6 @@ export function CreatePlacementsDialog({
</div> </div>
)} )}
{/* Existing project channels */}
{projectChannels.length > 0 && ( {projectChannels.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -432,7 +409,6 @@ export function CreatePlacementsDialog({
</div> </div>
)} )}
{/* Step 2: Details */}
{step === 2 && ( {step === 2 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -479,21 +455,20 @@ export function CreatePlacementsDialog({
onChange={(e) => setPlacementDate(e.target.value)} onChange={(e) => setPlacementDate(e.target.value)}
/> />
</div> </div>
</div>
<div className="space-y-2"> <div className="col-span-2 space-y-2">
<Label>Комментарий</Label> <Label>Комментарий</Label>
<Textarea <Textarea
placeholder="Дополнительные заметки..." placeholder="Дополнительные заметки..."
value={comment} value={comment}
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
rows={2} rows={2}
/> />
</div>
</div> </div>
</div> </div>
)} )}
{/* Step 3: Confirm */}
{step === 3 && ( {step === 3 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -547,7 +522,6 @@ export function CreatePlacementsDialog({
</div> </div>
)} )}
{/* Navigation */}
<DialogFooter> <DialogFooter>
{step > 1 && ( {step > 1 && (
<Button <Button