Files
tgex-frontend/lib/config/placement-columns.ts
2026-02-10 11:42:17 +03:00

233 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PlacementStatus, PlacementPostStatus, CostType, PlacementType, InviteLinkType } from "@/lib/types/api";
export interface ColumnConfig {
id: string;
label: string;
group: string;
editable: boolean;
partialEdit?: boolean;
width?: string;
required?: boolean;
}
export const COLUMN_GROUPS = [
{ id: "basic", label: "Основная информация" },
{ id: "content", label: "Контент" },
{ id: "finance", label: "Финансы" },
{ id: "dates", label: "Даты и время" },
{ id: "metrics", label: "Метрики" },
{ id: "calculated", label: "Расчетные показатели" },
{ id: "technical", label: "Технические детали" },
] as const;
export const ALL_COLUMNS: ColumnConfig[] = [
// Basic - узкие колонки
{ id: "checkbox", label: "", group: "basic", editable: false, required: true, width: "32px" },
{ id: "number", label: "№", group: "basic", editable: false, required: true, width: "40px" },
{ id: "status_deal", label: "Статус сделки", group: "basic", editable: true, partialEdit: true, width: "minmax(120px, 1.2fr)" },
{ id: "status_post", label: "Статус поста", group: "basic", editable: true, partialEdit: true, width: "minmax(120px, 1.2fr)" },
// Content
{ id: "creative", label: "Креатив", group: "content", editable: true, width: "minmax(140px, 1.5fr)" },
{ id: "invite_link", label: "Ссылка", group: "content", editable: false, width: "minmax(80px, 0.8fr)" },
{ id: "post_link", label: "Пост", group: "content", editable: false, width: "minmax(50px, 0.5fr)" },
// Finance
{ id: "cost", label: "Стоимость", group: "finance", editable: true, partialEdit: true, width: "minmax(80px, 0.8fr)" },
{ id: "cost_before", label: "До торга", group: "finance", editable: true, width: "minmax(80px, 0.8fr)" },
{ id: "cost_format", label: "Форм. оплаты", group: "finance", editable: true, partialEdit: true, width: "minmax(80px, 0.8fr)" },
{ id: "placement_type", label: "Тип закупа", group: "finance", editable: true, width: "minmax(100px, 1fr)" },
{ id: "discount", label: "Скидка", group: "finance", editable: false, width: "minmax(50px, 0.5fr)" },
{ id: "payment_date", label: "Оплата", group: "finance", editable: true, width: "minmax(90px, 0.8fr)" },
// Dates
{ id: "planned_date", label: "План. выход", group: "dates", editable: true, width: "minmax(90px, 0.8fr)" },
{ id: "actual_date", label: "Факт", group: "dates", editable: false, width: "minmax(90px, 0.8fr)" },
{ id: "format", label: "Формат", group: "dates", editable: true, width: "minmax(100px, 0.8fr)" },
{ id: "time_top", label: "В топе", group: "dates", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "time_in_feed", label: "В ленте", group: "dates", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "link_created", label: "Ссылка", group: "dates", editable: false, width: "minmax(80px, 0.7fr)" },
{ id: "post_deleted", label: "Удалён", group: "dates", editable: false, width: "minmax(80px, 0.7fr)" },
// Metrics
{ id: "subscriptions", label: "Подписки", group: "metrics", editable: false, width: "minmax(70px, 0.6fr)" },
{ id: "views", label: "Просмотры", group: "metrics", editable: false, width: "minmax(70px, 0.6fr)" },
// Calculated
{ id: "cpf", label: "CPF", group: "calculated", editable: false, width: "minmax(50px, 0.5fr)" },
{ id: "cpm", label: "CPM", group: "calculated", editable: true, partialEdit: true, width: "minmax(50px, 0.5fr)" },
{ id: "conversion_24h", label: "Конв. 24ч", group: "calculated", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "conversion_48h", label: "Конв. 48ч", group: "calculated", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "conversion_total", label: "Конв. общ.", group: "calculated", editable: false, width: "minmax(70px, 0.6fr)" },
{ id: "total_unsubs", label: "Отписки", group: "calculated", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "unsub_percent", label: "% отписок", group: "calculated", editable: false, width: "minmax(60px, 0.5fr)" },
{ id: "total_active", label: "Итого", group: "calculated", editable: false, width: "minmax(60px, 0.5fr)" },
// Technical
{ id: "invite_type", label: "Тип ссылки", group: "technical", editable: true, width: "minmax(100px, 1fr)" },
{ id: "comment", label: "Комментарий", group: "technical", editable: true, width: "minmax(150px, 2fr)" },
] as const;
// Default visible columns
export const DEFAULT_VISIBLE_COLUMNS = new Set([
"checkbox",
"number",
"status_deal",
"status_post",
"creative",
"invite_link",
"cost",
"cost_before",
"cost_format",
"planned_date",
"format",
// "subscriptions",
// "views",
// "cpf",
"cpm",
"comment",
]);
// Status lists
export const PLACEMENT_DEAL_STATUSES: PlacementStatus[] = [
"Без статуса",
"Написать",
"Ждём ответа",
"Согласование условий",
"Оплатить",
"Оплачено",
"Отмена",
"Не подходит цена",
"Неактуально",
"Не отвечает",
];
export const PLACEMENT_POST_STATUSES: PlacementPostStatus[] = [
"Без статуса",
"Отправить пост",
"Согласование поста",
"Ожидание отложки",
"Запланирован",
"Пост вышел",
"Размещение отработало - пост удалён",
"Размещение отработало - пост не удалён",
"Проверить - пост удалён раньше срока",
"Проверить - пост не вышел",
"Размещение отработало",
];
export const COST_FORMATS: CostType[] = ["fixed", "cpm"];
export const PLACEMENT_TYPES: PlacementType[] = ["self_promo", "standard"];
export const INVITE_TYPES: InviteLinkType[] = ["public", "approval"];
// Manual post statuses (user can select before post is published)
export const MANUAL_POST_STATUSES: Set<PlacementPostStatus> = new Set([
"Без статуса",
"Отправить пост",
"Согласование поста",
"Ожидание отложки",
"Запланирован",
]);
// Automatic post statuses (set by backend)
export const AUTOMATIC_POST_STATUSES: Set<PlacementPostStatus> = new Set([
"Пост вышел",
"Размещение отработало - пост удалён",
"Размещение отработало - пост не удалён",
"Проверить - пост удалён раньше срока",
"Проверить - пост не вышел",
"Размещение отработало",
]);
// Helper functions
export const canEditPostStatus = (currentStatus: PlacementPostStatus): boolean => {
return MANUAL_POST_STATUSES.has(currentStatus);
};
export const isAutomaticPostStatus = (status: PlacementPostStatus): boolean => {
return AUTOMATIC_POST_STATUSES.has(status);
};
// Status icon and color helpers
import {
CircleHelp,
FileText,
Clock,
DollarSign,
CheckCircle2,
XCircle,
AlertTriangle,
Send,
Calendar,
} from "lucide-react";
const StatusIconMap: Record<PlacementStatus, any> = {
"Без статуса": CircleHelp,
"Написать": FileText,
"Ждём ответа": Clock,
"Согласование условий": FileText,
"Оплатить": DollarSign,
"Оплачено": CheckCircle2,
"Отмена": XCircle,
"Не подходит цена": AlertTriangle,
"Неактуально": XCircle,
"Не отвечает": AlertTriangle,
};
const PostStatusIconMap: Record<PlacementPostStatus, any> = {
"Без статуса": CircleHelp,
"Отправить пост": Send,
"Согласование поста": FileText,
"Ожидание отложки": Clock,
"Запланирован": Calendar,
"Пост вышел": CheckCircle2,
"Размещение отработало - пост удалён": CheckCircle2,
"Размещение отработало - пост не удалён": CheckCircle2,
"Проверить - пост удалён раньше срока": AlertTriangle,
"Проверить - пост не вышел": Clock,
"Размещение отработало": CheckCircle2,
};
const StatusColorMap: Record<PlacementStatus, string> = {
"Без статуса": "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400 dark:border-gray-800",
"Написать": "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800",
"Ждём ответа": "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800",
"Согласование условий": "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800",
"Оплатить": "bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-400 dark:border-green-800",
"Оплачено": "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
"Отмена": "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800",
"Не подходит цена": "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800",
"Неактуально": "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400 dark:border-gray-800",
"Не отвечает": "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800",
};
const PostStatusColorMap: Record<PlacementPostStatus, string> = {
"Без статуса": "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400 dark:border-gray-800",
"Отправить пост": "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800",
"Согласование поста": "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-400 dark:border-purple-800",
"Ожидание отложки": "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:border-amber-800",
"Запланирован": "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400 dark:border-cyan-800",
"Пост вышел": "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
"Размещение отработало - пост удалён": "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
"Размещение отработало - пост не удалён": "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
"Проверить - пост удалён раньше срока": "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800",
"Проверить - пост не вышел": "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-400 dark:border-red-800",
"Размещение отработало": "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800",
};
export const getPlacementStatusIcon = (status: PlacementStatus): any => {
return StatusIconMap[status] || CircleHelp;
};
export const getPlacementStatusColor = (status: PlacementStatus): string => {
return StatusColorMap[status] || StatusColorMap["Без статуса"];
};
export const getPlacementPostStatusIcon = (status: PlacementPostStatus): any => {
return PostStatusIconMap[status] || CircleHelp;
};
export const getPlacementPostStatusColor = (status: PlacementPostStatus): string => {
return PostStatusColorMap[status] || PostStatusColorMap["Без статуса"];
};