From d87d6e25686a1bcf22bc8f1bebff322266569c05 Mon Sep 17 00:00:00 2001 From: ivannoskov Date: Tue, 3 Feb 2026 11:14:53 +0300 Subject: [PATCH] feat: purchase-plans update --- .../placements/[placementId]/page.tsx | 38 +++++-- .../purchase-plans/[projectId]/page.tsx | 101 +++++++++++++++--- components/placement-cells.tsx | 2 +- lib/api/placements.ts | 54 +++++++++- lib/config/placement-columns.ts | 15 +-- lib/demo/demo-aware-api.ts | 4 +- 6 files changed, 179 insertions(+), 35 deletions(-) diff --git a/app/dashboard/[workspaceId]/placements/[placementId]/page.tsx b/app/dashboard/[workspaceId]/placements/[placementId]/page.tsx index 30c91ca..ac25c47 100644 --- a/app/dashboard/[workspaceId]/placements/[placementId]/page.tsx +++ b/app/dashboard/[workspaceId]/placements/[placementId]/page.tsx @@ -19,7 +19,7 @@ import { } from "lucide-react"; import { DashboardHeader } from "@/components/layout/dashboard-header"; import { useWorkspace } from "@/components/providers/workspace-provider"; -import { placementsApi } from "@/lib/api"; +import { placementsApi, projectsApi } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { @@ -94,13 +94,35 @@ export default function PlacementDetailPage() { const loadPlacement = async () => { try { setLoading(true); - // First, try to load placement without project_id (deprecated endpoint) - // If that fails, we'd need projectId - const data = await placementsApi.get(workspaceId, placementId); - setPlacement(data); - // Store projectId from placement for future operations - if (data.project?.id) { - setProjectId(data.project.id); + + // First, get all projects to find which project this placement belongs to + const projectsResponse = await projectsApi.list(workspaceId, { size: 100 }); + const projects = projectsResponse.items; + + // Search for the placement in all projects + let foundProjectId: string | null = null; + let foundPlacement: any = null; + + for (const project of projects) { + try { + const placements = await placementsApi.getByProject(workspaceId, project.id); + const placement = placements.find((p) => p.id === placementId); + if (placement) { + foundProjectId = project.id; + foundPlacement = placement; + break; + } + } catch (e) { + // Skip if can't get placements for this project + continue; + } + } + + if (foundPlacement) { + setPlacement(foundPlacement); + setProjectId(foundProjectId); + } else { + console.error("Placement not found in any project"); } } catch (err) { console.error("Failed to load placement:", err); diff --git a/app/dashboard/[workspaceId]/purchase-plans/[projectId]/page.tsx b/app/dashboard/[workspaceId]/purchase-plans/[projectId]/page.tsx index 724d931..588b563 100644 --- a/app/dashboard/[workspaceId]/purchase-plans/[projectId]/page.tsx +++ b/app/dashboard/[workspaceId]/purchase-plans/[projectId]/page.tsx @@ -216,6 +216,7 @@ interface ChannelGroupProps { onOpenCreativeSelect?: (placement: PlacementWithStats) => void; onClearCreative?: (placementId: string) => void; onOpenCreativeSend?: (placement: PlacementWithStats) => void; + onAddPlacement?: (channel: Placement["channel"]) => void; } // ============================================================================ @@ -412,22 +413,22 @@ function CellRenderer({ ); case "creative": - const hasCreative = !!placement.creative_id; + const viewCreativeId = placement.creative_id; + const hasCreative = !!viewCreativeId; return ( {hasCreative ? ( -
-
- 📷 +
+
+ {placement.creative_name || "Без названия"}
- {placement.creative_name || "Без названия"}
+
@@ -1005,10 +1020,27 @@ function ChannelGroup({ key={column.id} className={cn( "py-2 px-3 font-medium text-muted-foreground", + column.editable && "cursor-help", column.width || "w-auto" )} > - {column.label} +
+ {column.editable && ( + +

+ {column.partialEdit ? ( + ( + ) + ) : ( + )} +

+
+ )} + {column.label} +
))} @@ -1025,8 +1057,12 @@ function ChannelGroup({ ? (effectiveCost / viewPlacement.placement_post.views_count) * 1000 : null; - return ( - + return ( + {ALL_COLUMNS.filter((c) => visibleColumns.has(c.id)).map((column) => ( { + if (!canEditPlacements) return; + + try { + const result = await placementsApi.createInProject(workspaceId, projectId, { + creative_id: undefined, + channels: [{ + channel_id: channel.id, + status: "Без статуса", + comment: undefined, + details: {}, + }], + details: {}, + }); + + // Extract the new placement from the response + const newPlacement = result.placements[0]; + + // Add the new placement to the list and switch to edit mode + setPlacements((prev) => [...prev, newPlacement]); + + // Scroll to and focus the new placement after a short delay + setTimeout(() => { + const rowElement = document.querySelector(`[data-placement-id="${newPlacement.id}"]`); + if (rowElement) { + rowElement.scrollIntoView({ behavior: "smooth", block: "center" }); + // Set editing state for the first editable field + setEditingCell({ placementId: newPlacement.id, field: "status" }); + } + }, 100); + } catch (err) { + console.error("Failed to add placement:", err); + } + }; + const groupedPlacements = useMemo(() => { const groups: Record = {}; @@ -1793,6 +1865,7 @@ export default function PurchasePlanDetailPage() { onOpenCreativeSelect={handleOpenCreativeSelect} onClearCreative={handleClearCreative} onOpenCreativeSend={handleOpenCreativeSend} + onAddPlacement={handleAddPlacement} /> ))}
diff --git a/components/placement-cells.tsx b/components/placement-cells.tsx index eb901b5..b794715 100644 --- a/components/placement-cells.tsx +++ b/components/placement-cells.tsx @@ -192,7 +192,7 @@ export const InviteLinkCell = ({ inviteLink, onCopy }: { inviteLink: string | nu {inviteLink ? (
- {inviteLink} + {inviteLink} diff --git a/lib/api/placements.ts b/lib/api/placements.ts index ac8303d..ad62cd1 100644 --- a/lib/api/placements.ts +++ b/lib/api/placements.ts @@ -33,10 +33,17 @@ export const placementsApi = { .then((response) => response.items), /** - * Get single placement (deprecated - use getInProject for project-specific data) + * Get single placement (requires project_id in original backend) + * Uses: GET /workspaces/{workspace_id}/projects/{project_id}/placements/{placement_id} */ - get: (workspaceId: string, placementId: string) => - api.get(`/workspaces/${workspaceId}/placements/${placementId}`), + get: ( + workspaceId: string, + placementId: string, + projectId?: string + ) => + api.get( + `/workspaces/${workspaceId}/projects/${projectId}/placements/${placementId}` + ), /** * Get single placement within project @@ -91,6 +98,47 @@ export const placementsApi = { data ), + /** + * Create single placement within project (bulk format) + * POST /workspaces/{workspace_id}/projects/{project_id}/placements + */ + createInProject: ( + workspaceId: string, + projectId: string, + data: { + creative_id?: string; + channels: Array<{ + channel_id: string; + status?: string; + comment?: string; + details?: { + placement_at?: string; + payment_at?: string; + cost?: { type: string; value: number }; + cost_before_bargain?: number; + placement_type?: string; + format?: string; + comment?: string; + creative_id?: string; + }; + }>; + details?: { + placement_at?: string; + payment_at?: string; + cost?: { type: string; value: number }; + cost_before_bargain?: number; + placement_type?: string; + format?: string; + comment?: string; + creative_id?: string; + }; + } + ) => + api.post<{ placements: Placement[] }>( + `/workspaces/${workspaceId}/projects/${projectId}/placements`, + data + ), + /** * Update placement */ diff --git a/lib/config/placement-columns.ts b/lib/config/placement-columns.ts index 41369b0..c35e740 100644 --- a/lib/config/placement-columns.ts +++ b/lib/config/placement-columns.ts @@ -5,6 +5,7 @@ export interface ColumnConfig { label: string; group: string; editable: boolean; + partialEdit?: boolean; width?: string; required?: boolean; } @@ -21,10 +22,10 @@ export const COLUMN_GROUPS = [ export const ALL_COLUMNS: ColumnConfig[] = [ // Basic - { id: "checkbox", label: "", group: "basic", editable: false, required: true }, - { id: "number", label: "№", group: "basic", editable: false, required: true }, - { id: "status_deal", label: "Статус сделки", group: "basic", editable: true }, - { id: "status_post", label: "Статус поста", group: "basic", editable: true }, + { id: "checkbox", label: "Чекбокс", group: "basic", editable: false, required: true }, + { id: "number", label: "Номер", group: "basic", editable: false, required: true }, + { id: "status_deal", label: "Статус сделки", group: "basic", editable: true, partialEdit: true }, + { id: "status_post", label: "Статус поста", group: "basic", editable: true, partialEdit: true }, // Content { id: "creative", label: "Креатив", group: "content", editable: true }, @@ -32,9 +33,9 @@ export const ALL_COLUMNS: ColumnConfig[] = [ { id: "post_link", label: "Ссылка на пост", group: "content", editable: false }, // Finance - { id: "cost", label: "Стоимость", group: "finance", editable: true }, + { id: "cost", label: "Стоимость", group: "finance", editable: true, partialEdit: true }, { id: "cost_before", label: "Стоимость до торга", group: "finance", editable: true }, - { id: "cost_format", label: "Формат оплаты", group: "finance", editable: true }, + { id: "cost_format", label: "Формат оплаты", group: "finance", editable: true, partialEdit: true }, { id: "placement_type", label: "Тип закупа", group: "finance", editable: true }, { id: "discount", label: "% скидки", group: "finance", editable: false }, { id: "payment_date", label: "Дата оплаты", group: "finance", editable: true }, @@ -54,7 +55,7 @@ export const ALL_COLUMNS: ColumnConfig[] = [ // Calculated { id: "cpf", label: "CPF", group: "calculated", editable: false }, - { id: "cpm", label: "CPM", group: "calculated", editable: false }, + { id: "cpm", label: "CPM", group: "calculated", editable: true, partialEdit: true }, { id: "conversion_24h", label: "Конверсия 24ч", group: "calculated", editable: false }, { id: "conversion_48h", label: "Конверсия 48ч", group: "calculated", editable: false }, { id: "conversion_total", label: "Конверсия общ.", group: "calculated", editable: false }, diff --git a/lib/demo/demo-aware-api.ts b/lib/demo/demo-aware-api.ts index 7cf2177..94c12d8 100644 --- a/lib/demo/demo-aware-api.ts +++ b/lib/demo/demo-aware-api.ts @@ -272,13 +272,13 @@ export const demoAwarePlacementsApi = { } return placementsApi.list(workspaceId, params); }, - get: async (workspaceId: string, placementId: string): Promise => { + get: async (workspaceId: string, placementId: string, _projectId?: string): Promise => { if (isDemoWorkspace(workspaceId)) { const placement = demoPlacements.find((p) => p.id === placementId); if (!placement) throw new Error("Placement not found"); return placement; } - return placementsApi.get(workspaceId, placementId); + return placementsApi.get(workspaceId, placementId, _projectId); }, getByProject: async (workspaceId: string, projectId: string): Promise => { if (isDemoWorkspace(workspaceId)) {