feat: global ui & functional update

This commit is contained in:
ivannoskov
2026-01-21 22:43:54 +03:00
parent 8958d89d12
commit 619fd5c1ef
32 changed files with 3803 additions and 1325 deletions

View File

@@ -113,14 +113,15 @@ export const demoApi = {
}
if (params?.placement_channel_id) {
items = items.filter(
(p) => p.placement_channel_id === params.placement_channel_id
(p) => p.channel.id === params.placement_channel_id
);
}
if (params?.creative_id) {
items = items.filter((p) => p.creative_id === params.creative_id);
}
// Filter out cancelled/archived placements
if (!params?.include_archived) {
items = items.filter((p) => p.status === "active");
items = items.filter((p) => p.status !== "Отмена" && p.status !== "Неактуально");
}
return Promise.resolve(paginate(items));
},
@@ -145,18 +146,18 @@ export const demoApi = {
// Filter by project if specified
if (params?.project_id) {
const projectPlacements = demoPlacements.filter(
(p) => p.project_id === params.project_id && p.status === "active"
(p) => p.project_id === params.project_id && p.status !== "Отмена" && p.status !== "Неактуально"
);
const totalCost = projectPlacements.reduce(
(sum, p) => sum + (p.cost || 0),
(sum, p) => sum + (p.details?.cost?.value || 0),
0
);
const totalSubs = projectPlacements.reduce(
(sum, p) => sum + (p.subscriptions_count || 0),
(sum, p) => sum + (p.placement_post?.subscriptions_count || 0),
0
);
const totalViews = projectPlacements.reduce(
(sum, p) => sum + (p.views_count || 0),
(sum, p) => sum + (p.placement_post?.views_count || 0),
0
);
@@ -167,6 +168,7 @@ export const demoApi = {
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
chart_data: demoSpendingAnalytics.chart_data.slice(0, 3),
placements_count: 12,
} as SpendingAnalytics);
}
return Promise.resolve(demoSpendingAnalytics);
@@ -187,7 +189,7 @@ export const demoApi = {
const projectPlacementChannelIds = new Set(
demoPlacements
.filter((p) => p.project_id === params.project_id)
.map((p) => p.placement_channel_id)
.map((p) => p.channel.id)
);
items = items.filter((a) => projectPlacementChannelIds.has(a.channel_id));
}

View File

@@ -8,6 +8,7 @@ import { ru } from "date-fns/locale";
import { isDemoWorkspace, DEMO_WORKSPACE_ID } from "./constants";
import {
demoChannels,
demoPlacementChannels,
demoCreatives,
demoPlacements,
demoPurchasePlanChannels,
@@ -124,6 +125,17 @@ export const demoAwareProjectsApi = {
}
return projectsApi.archive(workspaceId, projectId);
},
unarchive: async (
workspaceId: string,
projectId: string
): Promise<Project> => {
if (isDemoWorkspace(workspaceId)) {
const project = demoProjects.find(p => p.id === projectId);
if (!project) throw new Error("Project not found");
return { ...project, status: "active" };
}
return projectsApi.unarchive(workspaceId, projectId);
},
delete: async (
workspaceId: string,
projectId: string
@@ -138,6 +150,19 @@ export const demoAwareProjectsApi = {
}
return projectsApi.delete(workspaceId, projectId);
},
move: async (
workspaceId: string,
projectId: string,
data: { target_workspace_id: string }
): Promise<Project> => {
if (isDemoWorkspace(workspaceId)) {
const project = demoProjects.find(p => p.id === projectId);
if (!project) throw new Error("Project not found");
// In demo mode, just return the project (mock move)
return project;
}
return projectsApi.move(workspaceId, projectId, data);
},
};
// ============================================================================
@@ -234,14 +259,14 @@ export const demoAwarePlacementsApi = {
}
if (params?.placement_channel_id) {
items = items.filter(
(p) => p.placement_channel_id === params.placement_channel_id
(p) => p.channel.id === params.placement_channel_id
);
}
if (params?.creative_id) {
items = items.filter((p) => p.creative_id === params.creative_id);
}
if (!params?.include_archived) {
items = items.filter((p) => p.status === "active");
items = items.filter((p) => p.status !== "Отмена" && p.status !== "Неактуально");
}
return paginate(items, params?.page, params?.size);
}
@@ -255,6 +280,18 @@ export const demoAwarePlacementsApi = {
}
return placementsApi.get(workspaceId, placementId);
},
getByProject: async (workspaceId: string, projectId: string): Promise<Placement[]> => {
if (isDemoWorkspace(workspaceId)) {
// In demo mode, return all placements for demo project
// Filter by demo project id for realistic behavior
return demoPlacements.filter((p) => {
// For demo purposes, we'll just return all placements
// In real usage, this would filter by project_id
return true;
});
}
return placementsApi.getByProject(workspaceId, projectId);
},
};
// ============================================================================
@@ -292,18 +329,18 @@ export const demoAwareAnalyticsApi = {
if (isDemoWorkspace(workspaceId)) {
if (params?.project_id) {
const projectPlacements = demoPlacements.filter(
(p) => p.project_id === params.project_id && p.status === "active"
(p) => p.project_id === params.project_id && p.status !== "Отмена" && p.status !== "Неактуально"
);
const totalCost = projectPlacements.reduce(
(sum, p) => sum + (p.cost || 0),
(sum, p) => sum + (p.details?.cost?.value || 0),
0
);
const totalSubs = projectPlacements.reduce(
(sum, p) => sum + (p.subscriptions_count || 0),
(sum, p) => sum + (p.placement_post?.subscriptions_count || 0),
0
);
const totalViews = projectPlacements.reduce(
(sum, p) => sum + (p.views_count || 0),
(sum, p) => sum + (p.placement_post?.views_count || 0),
0
);
return {
@@ -313,6 +350,7 @@ export const demoAwareAnalyticsApi = {
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
chart_data: demoSpendingAnalytics.chart_data.slice(0, 4),
placements_count: projectPlacements.length,
};
}
return demoSpendingAnalytics;
@@ -345,7 +383,7 @@ export const demoAwareAnalyticsApi = {
const projectPlacementChannelIds = new Set(
demoPlacements
.filter((p) => p.project_id === params.project_id)
.map((p) => p.placement_channel_id)
.map((p) => p.channel.id)
);
items = items.filter((a) =>
projectPlacementChannelIds.has(a.channel_id)
@@ -357,13 +395,35 @@ export const demoAwareAnalyticsApi = {
},
placements: async (
workspaceId: string,
params?: { project_id?: string; page?: number; size?: number }
params?: {
project_id?: string;
placement_channel_id?: string;
creative_id?: string;
date_from?: string;
date_to?: string;
page?: number;
size?: number;
}
): Promise<PaginatedResponse<PlacementAnalyticsItem>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoPlacementAnalytics;
if (params?.project_id) {
items = items.filter((p) => p.project_id === params.project_id);
}
if (params?.placement_channel_id) {
items = items.filter((p) => p.placement_channel_id === params.placement_channel_id);
}
if (params?.creative_id) {
items = items.filter((p) => p.creative_id === params.creative_id);
}
if (params?.date_from) {
const from = new Date(params.date_from);
items = items.filter((p) => new Date(p.placement_date) >= from);
}
if (params?.date_to) {
const to = new Date(params.date_to);
items = items.filter((p) => new Date(p.placement_date) <= to);
}
return paginate(items, params?.page, params?.size);
}
return analyticsApi.placements(workspaceId, params);

View File

@@ -165,18 +165,18 @@ export const useDemoSpendingAnalytics = (
if (projectId) {
// Filter by project
const projectPlacements = demoPlacements.filter(
(p) => p.project_id === projectId && p.status === "active"
(p) => p.project_id === projectId && p.status !== "Отмена" && p.status !== "Неактуально"
);
const totalCost = projectPlacements.reduce(
(sum, p) => sum + (p.cost || 0),
(sum, p) => sum + (p.details?.cost?.value || 0),
0
);
const totalSubs = projectPlacements.reduce(
(sum, p) => sum + (p.subscriptions_count || 0),
(sum, p) => sum + (p.placement_post?.subscriptions_count || 0),
0
);
const totalViews = projectPlacements.reduce(
(sum, p) => sum + (p.views_count || 0),
(sum, p) => sum + (p.placement_post?.views_count || 0),
0
);
@@ -188,6 +188,7 @@ export const useDemoSpendingAnalytics = (
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
chart_data: demoSpendingAnalytics.chart_data.slice(0, 3),
placements_count: 12,
},
isDemo: true,
};
@@ -250,7 +251,7 @@ export const useDemoChannelAnalytics = (
const projectPlacementChannelIds = new Set(
demoPlacements
.filter((p) => p.project_id === projectId)
.map((p) => p.placement_channel_id)
.map((p) => p.channel.id)
);
items = items.filter((a) => projectPlacementChannelIds.has(a.channel_id));
}

View File

@@ -8,6 +8,7 @@ import type {
Channel,
Creative,
Placement,
PlacementChannel,
PurchasePlanChannel,
WorkspaceMember,
Permission,
@@ -187,139 +188,6 @@ export const demoCreatives: Creative[] = [
},
];
// ============================================================================
// Placements
// ============================================================================
export const demoPlacements: Placement[] = [
{
id: "plac-0001-0000-0000-000000000001",
placement_date: "2024-12-20T12:00:00Z",
cost: 5500,
comment: "Отличное размещение",
ad_post_url: "https://t.me/tech_future/1234",
invite_link_type: "approval",
invite_link: "https://t.me/+ABC123demo1",
status: "active",
subscriptions_count: 156,
views_count: 8500,
views_availability: "available",
last_views_fetch_at: "2024-12-21T10:00:00Z",
created_at: "2024-12-19T15:00:00Z",
project_id: "proj-0001-0000-0000-000000000001",
project_channel_title: "Крипто Новости",
placement_channel_id: "chan-0001-0000-0000-000000000001",
placement_channel_title: "Технологии Будущего",
creative_id: "crea-0001-0000-0000-000000000001",
creative_name: "Крипто - Основной",
},
{
id: "plac-0002-0000-0000-000000000002",
placement_date: "2024-12-18T14:00:00Z",
cost: 3200,
comment: null,
ad_post_url: "https://t.me/invest_pro/5678",
invite_link_type: "approval",
invite_link: "https://t.me/+ABC123demo2",
status: "active",
subscriptions_count: 89,
views_count: 4200,
views_availability: "available",
last_views_fetch_at: "2024-12-20T08:00:00Z",
created_at: "2024-12-17T10:00:00Z",
project_id: "proj-0001-0000-0000-000000000001",
project_channel_title: "Крипто Новости",
placement_channel_id: "chan-0002-0000-0000-000000000002",
placement_channel_title: "Инвестиции Pro",
creative_id: "crea-0001-0000-0000-000000000001",
creative_name: "Крипто - Основной",
},
{
id: "plac-0003-0000-0000-000000000003",
placement_date: "2024-12-15T10:00:00Z",
cost: 4800,
comment: "Программисты - хорошая аудитория",
ad_post_url: "https://t.me/programmers_hub/9012",
invite_link_type: "public",
invite_link: "https://t.me/+ABC123demo3",
status: "active",
subscriptions_count: 234,
views_count: 12000,
views_availability: "available",
last_views_fetch_at: "2024-12-19T14:00:00Z",
created_at: "2024-12-14T09:00:00Z",
project_id: "proj-0002-0000-0000-000000000002",
project_channel_title: "IT Вакансии",
placement_channel_id: "chan-0003-0000-0000-000000000003",
placement_channel_title: "Программисты",
creative_id: "crea-0003-0000-0000-000000000003",
creative_name: "IT Jobs - Основной",
},
{
id: "plac-0004-0000-0000-000000000004",
placement_date: "2024-12-10T16:00:00Z",
cost: 2500,
comment: null,
ad_post_url: "https://t.me/marketing_pro/3456",
invite_link_type: "approval",
invite_link: "https://t.me/+ABC123demo4",
status: "active",
subscriptions_count: 67,
views_count: 3100,
views_availability: "manual",
last_views_fetch_at: "2024-12-12T11:00:00Z",
created_at: "2024-12-09T14:00:00Z",
project_id: "proj-0003-0000-0000-000000000003",
project_channel_title: "Стартапы и Бизнес",
placement_channel_id: "chan-0004-0000-0000-000000000004",
placement_channel_title: "Маркетинг Pro",
creative_id: "crea-0004-0000-0000-000000000004",
creative_name: "Startups - Нетворкинг",
},
{
id: "plac-0005-0000-0000-000000000005",
placement_date: "2024-12-05T11:00:00Z",
cost: 6200,
comment: "Финансовая аудитория",
ad_post_url: "https://t.me/finance_edu/7890",
invite_link_type: "approval",
invite_link: "https://t.me/+ABC123demo5",
status: "active",
subscriptions_count: 312,
views_count: 15600,
views_availability: "available",
last_views_fetch_at: "2024-12-08T09:00:00Z",
created_at: "2024-12-04T10:00:00Z",
project_id: "proj-0001-0000-0000-000000000001",
project_channel_title: "Крипто Новости",
placement_channel_id: "chan-0005-0000-0000-000000000005",
placement_channel_title: "Финансовая грамотность",
creative_id: "crea-0002-0000-0000-000000000002",
creative_name: "Крипто - Агрессивный",
},
{
id: "plac-0006-0000-0000-000000000006",
placement_date: "2024-11-28T13:00:00Z",
cost: 1800,
comment: null,
ad_post_url: "https://t.me/startup_daily/2345",
invite_link_type: "public",
invite_link: "https://t.me/+ABC123demo6",
status: "archived",
subscriptions_count: 45,
views_count: 2100,
views_availability: "unavailable",
last_views_fetch_at: null,
created_at: "2024-11-27T15:00:00Z",
project_id: "proj-0003-0000-0000-000000000003",
project_channel_title: "Стартапы и Бизнес",
placement_channel_id: "chan-0006-0000-0000-000000000006",
placement_channel_title: "Startup Daily",
creative_id: "crea-0004-0000-0000-000000000004",
creative_name: "Startups - Нетворкинг",
},
];
// ============================================================================
// Purchase Plan Channels
// ============================================================================
@@ -363,60 +231,204 @@ export const demoPurchasePlanChannels: Record<string, PurchasePlanChannel[]> = {
},
},
],
"proj-0002-0000-0000-000000000002": [
{
id: "plan-0004-0000-0000-000000000004",
status: "completed",
planned_cost: 4800,
comment: "IT аудитория",
channel: {
id: "chan-0003-0000-0000-000000000003",
telegram_id: -1002345678003,
title: "Программисты",
username: "programmers_hub",
},
},
{
id: "plan-0005-0000-0000-000000000005",
status: "planned",
planned_cost: 3000,
comment: null,
channel: {
id: "chan-0001-0000-0000-000000000001",
telegram_id: -1002345678001,
title: "Технологии Будущего",
username: "tech_future",
},
},
],
"proj-0003-0000-0000-000000000003": [
{
id: "plan-0006-0000-0000-000000000006",
status: "completed",
planned_cost: 2500,
comment: null,
channel: {
id: "chan-0004-0000-0000-000000000004",
telegram_id: -1002345678004,
title: "Маркетинг Pro",
username: "marketing_pro",
},
},
{
id: "plan-0007-0000-0000-000000000007",
status: "completed",
planned_cost: 1800,
comment: "Стартап аудитория",
channel: {
id: "chan-0006-0000-0000-000000000006",
telegram_id: -1002345678006,
title: "Startup Daily",
username: "startup_daily",
},
},
],
};
// ============================================================================
// Placements for Project (New Format)
// ============================================================================
export const demoPlacementChannels: PlacementChannel[] = [
{
id: "chan-0001-0000-0000-000000000001",
telegram_id: -1001234001001,
title: "Технологии Будущего",
username: "tech_future",
subscribers_count: 125000,
},
{
id: "chan-0002-0000-0000-000000000002",
telegram_id: -1001234001002,
title: "Инвестиции Pro",
username: "invest_pro",
subscribers_count: 89000,
},
{
id: "chan-0003-0000-0000-000000000003",
telegram_id: -1001234001003,
title: "Программисты",
username: "programmers",
subscribers_count: 230000,
},
{
id: "chan-0004-0000-0000-000000000004",
telegram_id: -1001234001004,
title: "Маркетинг Pro",
username: "marketing_pro",
subscribers_count: 67000,
},
];
export const demoPlacements: (Placement & { project_id: string })[] = [
{
id: "plac-0001-0000-0000-000000000001",
project_id: "proj-0001-0000-0000-000000000001",
status: "Оплачено",
creative_id: "crea-0001-0000-0000-000000000001",
comment: "crypto_jan",
invite_link: "https://t.me/+abc123",
invite_link_type: "public",
channel: demoPlacementChannels[0],
details: {
placement_at: "2024-12-20T12:00:00Z",
payment_at: "2024-12-19T10:00:00Z",
cost: { type: "fixed", value: 5500 },
cost_before_bargain: 6000,
placement_type: "self_promo",
format: "Стандарт",
comment: null,
creative_id: "crea-0001-0000-0000-000000000001",
},
placement_post: {
subscriptions_count: 156,
views_count: 8500,
created_at: "2024-12-20T12:00:00Z",
post: {
id: "post-0001-0000-0000-000000000001",
message_id: 12345,
text: "Привет! Отличные новости...",
url: "https://t.me/tech_future/12345",
deleted_from_channel_at: null,
created_at: "2024-12-20T12:00:00Z",
updated_at: "2024-12-20T12:00:00Z",
},
},
created_at: "2024-12-18T08:00:00Z",
},
{
id: "plac-0002-0000-0000-000000000002",
project_id: "proj-0001-0000-0000-000000000001",
status: "Согласование условий",
creative_id: "crea-0001-0000-0000-000000000001",
comment: "crypto_feb",
invite_link: "https://t.me/+def456",
invite_link_type: "approval",
channel: demoPlacementChannels[1],
details: {
placement_at: "2024-12-25T14:00:00Z",
payment_at: null,
cost: { type: "fixed", value: 3200 },
cost_before_bargain: 3500,
placement_type: "self_promo",
format: "Стандарт",
comment: null,
creative_id: "crea-0001-0000-0000-000000000001",
},
placement_post: null,
created_at: "2024-12-22T09:00:00Z",
},
{
id: "plac-0003-0000-0000-000000000003",
project_id: "proj-0001-0000-0000-000000000001",
status: "Оплачено",
creative_id: "crea-0001-0000-0000-000000000001",
comment: null,
invite_link: "https://t.me/+ghi789",
invite_link_type: "public",
channel: demoPlacementChannels[2],
details: {
placement_at: "2024-12-15T10:00:00Z",
payment_at: "2024-12-14T16:00:00Z",
cost: { type: "fixed", value: 4800 },
cost_before_bargain: 5000,
placement_type: "self_promo",
format: "Стандарт",
comment: null,
creative_id: "crea-0001-0000-0000-000000000001",
},
placement_post: {
subscriptions_count: 234,
views_count: 12000,
created_at: "2024-12-15T10:00:00Z",
post: {
id: "post-0002-0000-0000-000000000002",
message_id: 67890,
text: "Криптовалюты продолжают расти...",
url: "https://t.me/programmers/67890",
deleted_from_channel_at: null,
created_at: "2024-12-15T10:00:00Z",
updated_at: "2024-12-15T10:00:00Z",
},
},
created_at: "2024-12-12T11:00:00Z",
},
{
id: "plac-0004-0000-0000-000000000004",
project_id: "proj-0001-0000-0000-000000000001",
status: "Ждём ответа",
creative_id: "crea-0001-0000-0000-000000000001",
comment: null,
invite_link: "https://t.me/+jkl012",
invite_link_type: "public",
channel: demoPlacementChannels[3],
details: {
placement_at: "2024-12-28T09:00:00Z",
payment_at: null,
cost: { type: "fixed", value: 2500 },
cost_before_bargain: 2800,
placement_type: "self_promo",
format: "Стандарт",
comment: null,
creative_id: "crea-0001-0000-0000-000000000001",
},
placement_post: null,
created_at: "2024-12-24T14:00:00Z",
},
{
id: "plac-0005-0000-0000-000000000005",
project_id: "proj-0002-0000-0000-000000000002",
status: "Написать",
creative_id: "crea-0002-0000-0000-000000000002",
comment: "Новый оффер",
invite_link: null,
invite_link_type: "approval",
channel: demoPlacementChannels[0],
details: {
placement_at: null,
payment_at: null,
cost: { type: "fixed", value: 6000 },
cost_before_bargain: null,
placement_type: "self_promo",
format: "Спонсорский",
comment: null,
creative_id: "crea-0002-0000-0000-000000000002",
},
placement_post: null,
created_at: "2024-12-26T08:00:00Z",
},
{
id: "plac-0006-0000-0000-000000000006",
project_id: "proj-0002-0000-0000-000000000002",
status: "Отмена",
creative_id: "crea-0001-0000-0000-000000000001",
comment: "Неактуально",
invite_link: null,
invite_link_type: "public",
channel: demoPlacementChannels[1],
details: {
placement_at: null,
payment_at: null,
cost: null,
cost_before_bargain: null,
placement_type: null,
format: null,
comment: "Клиент отказался",
creative_id: null,
},
placement_post: null,
created_at: "2024-12-10T10:00:00Z",
},
];
// ============================================================================
// Analytics Data
// ============================================================================
@@ -427,6 +439,7 @@ export const demoSpendingAnalytics: SpendingAnalytics = {
total_views: 45500,
avg_cpf: 26.58,
avg_cpm: 527.47,
placements_count: 12,
chart_data: [
{
period: "2024-11",
@@ -607,6 +620,7 @@ export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
views_count: 8500,
cpf: 35.26,
cpm: 647.06,
post_url: "https://t.me/tech_future/1001",
},
{
id: "plac-0002-0000-0000-000000000002",
@@ -622,6 +636,7 @@ export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
views_count: 4200,
cpf: 35.96,
cpm: 761.9,
post_url: "https://t.me/invest_pro/502",
},
{
id: "plac-0003-0000-0000-000000000003",
@@ -637,6 +652,7 @@ export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
views_count: 12000,
cpf: 20.51,
cpm: 400.0,
post_url: "https://t.me/programmers_hub/2050",
},
{
id: "plac-0004-0000-0000-000000000004",
@@ -652,6 +668,7 @@ export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
views_count: 3100,
cpf: 37.31,
cpm: 806.45,
post_url: "https://t.me/marketing_pro/803",
},
{
id: "plac-0005-0000-0000-000000000005",
@@ -667,6 +684,7 @@ export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
views_count: 15600,
cpf: 19.87,
cpm: 397.44,
post_url: "https://t.me/finance_edu/1204",
},
];