fix: creatre placement fix
This commit is contained in:
@@ -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,9 +455,8 @@ 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="Дополнительные заметки..."
|
||||||
@@ -491,9 +466,9 @@ export function CreatePlacementsDialog({
|
|||||||
/>
|
/>
|
||||||
</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
|
||||||
|
|||||||
Reference in New Issue
Block a user