import { Check, X, Copy, Send, ExternalLink, Play } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import type { PlacementWithStats, PlacementStatus, PlacementPostStatus, CostType, PlacementType, InviteLinkType } from "@/lib/types/api"; import { PLACEMENT_DEAL_STATUSES, PLACEMENT_POST_STATUSES, COST_FORMATS, PLACEMENT_TYPES, INVITE_TYPES, canEditPostStatus } from "@/lib/config/placement-columns"; // ============================================================================ // Cell Renderers // ============================================================================ interface CellProps { placement: PlacementWithStats; idx: number; channelNumber: number; canEdit: boolean; isEditing?: boolean; onEdit?: () => void; onSave?: (value: any) => void; } // Checkbox column export const CheckboxCell = ({ checked, onCheckedChange, onClick }: { checked: boolean; onCheckedChange: () => void; onClick?: (e: React.MouseEvent) => void }) => ( ); // Number column export const NumberCell = ({ channelNumber, idx }: { channelNumber: number; idx: number }) => ( {channelNumber}.{idx + 1} ); // Status deal column export const StatusDealCell = ({ status, canEdit, isEditing, onEdit, onSave, }: { status: PlacementStatus; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: PlacementStatus) => void; }) => { const getStatusColor = (s: PlacementStatus): string => { switch (s) { case "Оплачено": return "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800"; case "Согласование условий": case "Ждём ответа": return "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800"; case "Отмена": case "Не подходит цена": case "Неактуально": case "Не отвечает": return "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800"; case "Оплатить": return "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800"; default: return "bg-muted text-muted-foreground border-border"; } }; return ( {isEditing ? ( ) : ( )} ); }; // Status post column export const StatusPostCell = ({ status, canEdit, isEditing, onEdit, onSave, }: { status: PlacementPostStatus; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: PlacementPostStatus) => void; }) => { const canEditStatus = canEdit && canEditPostStatus(status); const isAuto = !canEditPostStatus(status); return ( {isEditing && canEditStatus ? ( ) : (
{status}
)} ); }; // Creative column export const CreativeCell = ({ creativeId, creativeName, canEdit, onToggleCreative, }: { creativeId: string | null; creativeName: string | null; canEdit: boolean; onToggleCreative: () => void; }) => ( {creativeId ? (
📷
{creativeName || "Без названия"}
) : ( )} ); // Invite link column export const InviteLinkCell = ({ inviteLink, onCopy }: { inviteLink: string | null; onCopy: () => void }) => ( {inviteLink ? (
{inviteLink}
) : ( )} ); // Cost column export const CostCell = ({ cost, costType, canEdit, isEditing, onEdit, onSave, }: { cost: number | null; costType: CostType | null; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: { type: CostType; value: number } | null) => void; }) => ( {isEditing ? ( { const raw = e.target.value; if (!raw) { onSave(null); return; } const value = Number(raw); if (Number.isNaN(value)) return; onSave({ type: costType || "fixed", value }); }} onBlur={(e) => { e.currentTarget.blur(); }} /> ) : ( )} ); // Date column export const DateCell = ({ date, canEdit, isEditing, onEdit, onSave, }: { date: string | null; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: string | null) => void; }) => { const formatDate = (d: string | null): string => { if (!d) return "—"; return new Date(d).toLocaleDateString("ru-RU", { day: "2-digit", month: "short", year: "numeric", }); }; const toDateInputValue = (iso: string | null): string => { if (!iso) return ""; const date = new Date(iso); if (Number.isNaN(date.getTime())) return ""; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; const toIsoFromDateInput = (value: string): string | null => { if (!value) return null; const date = new Date(`${value}T00:00:00.000Z`); if (Number.isNaN(date.getTime())) return null; return date.toISOString(); }; return ( {isEditing ? ( onSave(toIsoFromDateInput(e.target.value))} onBlur={(e) => e.currentTarget.blur()} /> ) : ( )} ); }; // Text column (for format, comment) export const TextCell = ({ value, placeholder = "—", canEdit, isEditing, onEdit, onSave, className = "", }: { value: string | null; placeholder?: string; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: string | null) => void; className?: string; }) => ( {isEditing ? ( onSave(e.target.value || null)} onBlur={(e) => e.currentTarget.blur()} /> ) : ( )} ); // Metrics column export const MetricCell = ({ value, format = "number" }: { value: number | null; format?: "number" | "currency" | "percent" }) => { const formatValue = () => { if (value === null || value === undefined) return "—"; switch (format) { case "currency": return new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); case "percent": return `${value.toFixed(1)}%`; case "number": default: return new Intl.NumberFormat("ru-RU").format(value); } }; return ( {formatValue()} ); }; // Select column (for cost_format, placement_type, invite_type) export const SelectCell = ({ value, options, canEdit, isEditing, onEdit, onSave, displayMap, }: { value: string | null; options: string[]; canEdit: boolean; isEditing: boolean; onEdit: () => void; onSave: (value: string) => void; displayMap?: Record; }) => { const getDisplayValue = (v: string) => { if (displayMap) return displayMap[v] || v; switch (v) { case "fixed": return "Фикс"; case "cpm": return "CPM"; case "self_promo": return "Взаимный пиар"; case "standard": return "Стандартный"; case "public": return "Открытая"; case "approval": return "С заявками"; default: return v; } }; return ( {isEditing ? ( ) : ( )} ); }; // Link column export const LinkCell = ({ url, external = false }: { url: string | null; external?: boolean }) => ( {url ? ( {url.substring(0, 50)} {url.length > 50 ? "..." : ""} {external && } ) : ( )} ); // Empty column (for future columns not yet implemented) export const EmptyCell = ({ label }: { label?: string }) => ( {label || "Недоступно"} );