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 || "Недоступно"}
|
);