feat: purchase-plans update
This commit is contained in:
@@ -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 (
|
||||
<td key={column.id} className="py-2 px-3">
|
||||
{hasCreative ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded bg-muted flex items-center justify-center overflow-hidden shrink-0">
|
||||
<span className="text-xs">📷</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2 px-2 py-1 bg-muted/50 rounded ">
|
||||
<span className="text-xs font-medium truncate max-w-[100px]">{placement.creative_name || "Без названия"}</span>
|
||||
</div>
|
||||
<span className="text-xs truncate max-w-[100px]">{placement.creative_name || "Без названия"}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-primary"
|
||||
title="Отправить креатив"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-primary rounded"
|
||||
title="Отправить готовый креатив"
|
||||
onClick={() => {
|
||||
if (placement.creative_id) {
|
||||
if (viewCreativeId) {
|
||||
onOpenCreativeSend?.(placement);
|
||||
}
|
||||
}}
|
||||
@@ -437,10 +438,10 @@ function CellRenderer({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive rounded"
|
||||
title="Очистить креатив"
|
||||
onClick={() => {
|
||||
if (placement.creative_id) {
|
||||
if (viewCreativeId) {
|
||||
onClearCreative?.(placement.id);
|
||||
}
|
||||
}}
|
||||
@@ -469,7 +470,7 @@ function CellRenderer({
|
||||
return (
|
||||
<td key={column.id} className="py-2 px-3">
|
||||
{placement.invite_link ? (
|
||||
<code className="text-xs bg-muted px-2 py-1 rounded truncate max-w-[200px]">
|
||||
<code className="text-xs bg-muted/50 px-2 py-1 rounded truncate max-w-[200px]">
|
||||
{placement.invite_link}
|
||||
</code>
|
||||
) : (
|
||||
@@ -905,6 +906,7 @@ function ChannelGroup({
|
||||
onOpenCreativeSelect,
|
||||
onClearCreative,
|
||||
onOpenCreativeSend,
|
||||
onAddPlacement,
|
||||
}: ChannelGroupProps) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
@@ -993,6 +995,19 @@ function ChannelGroup({
|
||||
{formatCompact(totalViews)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-3 text-xs ml-4"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddPlacement?.(channel);
|
||||
}}
|
||||
title="Добавить размещение"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
@@ -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}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{column.editable && (
|
||||
<span
|
||||
className="flex items-center gap-0.5"
|
||||
title={column.partialEdit ? "Частично редактируемое" : "Редактируемое"}
|
||||
>
|
||||
<p className="flex items-center whitespace-nowrap">
|
||||
{column.partialEdit ? (
|
||||
<span className="text-[10px] opacity-40 flex items-center gap-0.5">(
|
||||
<Pencil className="h-3 w-3 " />)</span>
|
||||
) : (
|
||||
<Pencil className="h-3 w-3 opacity-40" />)}
|
||||
</p>
|
||||
</span>
|
||||
)}
|
||||
<span>{column.label}</span>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -1025,8 +1057,12 @@ function ChannelGroup({
|
||||
? (effectiveCost / viewPlacement.placement_post.views_count) * 1000
|
||||
: null;
|
||||
|
||||
return (
|
||||
<tr key={placement.id} className="border-b last:border-0 hover:bg-muted/30">
|
||||
return (
|
||||
<tr
|
||||
key={placement.id}
|
||||
data-placement-id={placement.id}
|
||||
className="border-b last:border-0 hover:bg-muted/30"
|
||||
>
|
||||
{ALL_COLUMNS.filter((c) => visibleColumns.has(c.id)).map((column) => (
|
||||
<CellRenderer
|
||||
key={column.id}
|
||||
@@ -1335,6 +1371,7 @@ export default function PurchasePlanDetailPage() {
|
||||
}
|
||||
|
||||
await loadPlacements();
|
||||
setSavingEdits(false);
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to save placement edits:", err);
|
||||
const message =
|
||||
@@ -1422,6 +1459,41 @@ export default function PurchasePlanDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddPlacement = async (channel: Placement["channel"]) => {
|
||||
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<string, { channel: Placement["channel"]; placements: PlacementWithStats[] }> = {};
|
||||
|
||||
@@ -1793,6 +1865,7 @@ export default function PurchasePlanDetailPage() {
|
||||
onOpenCreativeSelect={handleOpenCreativeSelect}
|
||||
onClearCreative={handleClearCreative}
|
||||
onOpenCreativeSend={handleOpenCreativeSend}
|
||||
onAddPlacement={handleAddPlacement}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user