feat: update analytics
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
// ============================================================================
|
||||
// This wrapper checks if the workspace is demo and returns mock data instead
|
||||
|
||||
import { format, getISOWeek } from "date-fns";
|
||||
import { ru } from "date-fns/locale";
|
||||
import { isDemoWorkspace, DEMO_WORKSPACE_ID } from "./constants";
|
||||
import {
|
||||
demoChannels,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
demoWorkspace,
|
||||
demoMember,
|
||||
demoOverviewAnalytics,
|
||||
demoPlacementAnalytics,
|
||||
} from "./mock-data";
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
@@ -31,6 +34,9 @@ import type {
|
||||
WorkspaceMember,
|
||||
DateGrouping,
|
||||
OverviewAnalytics,
|
||||
PlacementAnalyticsItem,
|
||||
ProjectsAnalyticsResponse,
|
||||
ProjectsAnalyticsQueryParams,
|
||||
} from "@/lib/types/api";
|
||||
import {
|
||||
workspacesApi,
|
||||
@@ -349,6 +355,19 @@ export const demoAwareAnalyticsApi = {
|
||||
}
|
||||
return analyticsApi.channels(workspaceId, params);
|
||||
},
|
||||
placements: async (
|
||||
workspaceId: string,
|
||||
params?: { project_id?: 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);
|
||||
}
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return analyticsApi.placements(workspaceId, params);
|
||||
},
|
||||
overview: async (
|
||||
workspaceId: string,
|
||||
params?: { project_id?: string; date_from?: string; date_to?: string }
|
||||
@@ -358,5 +377,145 @@ export const demoAwareAnalyticsApi = {
|
||||
}
|
||||
return analyticsApi.overview(workspaceId, params);
|
||||
},
|
||||
projects: async (
|
||||
workspaceId: string,
|
||||
params: ProjectsAnalyticsQueryParams
|
||||
): Promise<ProjectsAnalyticsResponse> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
// Generate mock data based on demoPlacementAnalytics
|
||||
const placements = demoPlacementAnalytics;
|
||||
let filtered = placements;
|
||||
|
||||
if (params.project_ids && params.project_ids.length > 0) {
|
||||
filtered = filtered.filter((p) => params.project_ids!.includes(p.project_id));
|
||||
}
|
||||
|
||||
if (params.date_from) {
|
||||
const from = new Date(params.date_from);
|
||||
filtered = filtered.filter((p) => new Date(p.placement_date) >= from);
|
||||
}
|
||||
|
||||
if (params.date_to) {
|
||||
const to = new Date(params.date_to);
|
||||
filtered = filtered.filter((p) => new Date(p.placement_date) <= to);
|
||||
}
|
||||
|
||||
// Helper to format period label
|
||||
const formatPeriodLabel = (period: string, grouping: string): string => {
|
||||
try {
|
||||
if (grouping === "day") {
|
||||
const date = new Date(period + "T00:00:00");
|
||||
return format(date, "d MMM", { locale: ru });
|
||||
} else if (grouping === "week") {
|
||||
return period.replace("W", " нед. ");
|
||||
} else if (grouping === "month") {
|
||||
const date = new Date(period + "-01");
|
||||
return format(date, "MMM yyyy", { locale: ru });
|
||||
} else if (grouping === "quarter") {
|
||||
return period.replace("Q", "Q");
|
||||
}
|
||||
return period;
|
||||
} catch {
|
||||
return period;
|
||||
}
|
||||
};
|
||||
|
||||
// Group by period
|
||||
const grouped: Record<string, typeof filtered> = {};
|
||||
filtered.forEach((item) => {
|
||||
const date = new Date(item.placement_date);
|
||||
let periodKey: string;
|
||||
|
||||
switch (params.grouping) {
|
||||
case "day":
|
||||
periodKey = format(date, "yyyy-MM-dd");
|
||||
break;
|
||||
case "week":
|
||||
const weekNum = getISOWeek(date);
|
||||
periodKey = `${date.getFullYear()}-W${weekNum.toString().padStart(2, "0")}`;
|
||||
break;
|
||||
case "month":
|
||||
periodKey = format(date, "yyyy-MM");
|
||||
break;
|
||||
case "quarter":
|
||||
const quarter = Math.floor(date.getMonth() / 3) + 1;
|
||||
periodKey = `${date.getFullYear()}-Q${quarter}`;
|
||||
break;
|
||||
default:
|
||||
periodKey = format(date, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
if (!grouped[periodKey]) {
|
||||
grouped[periodKey] = [];
|
||||
}
|
||||
grouped[periodKey].push(item);
|
||||
});
|
||||
|
||||
// Calculate metrics for each period
|
||||
const periods = Object.entries(grouped)
|
||||
.map(([period, items]) => {
|
||||
const totalCost = items.reduce((sum, item) => sum + (item.cost || 0), 0);
|
||||
const totalSubs = items.reduce((sum, item) => sum + (item.subscriptions_count || 0), 0);
|
||||
const totalViews = items.reduce((sum, item) => sum + (item.views_count || 0), 0);
|
||||
const purchasesCount = items.length;
|
||||
|
||||
return {
|
||||
period,
|
||||
period_label: formatPeriodLabel(period, params.grouping),
|
||||
metrics: {
|
||||
total_cost: totalCost,
|
||||
purchases_count: purchasesCount,
|
||||
total_subscriptions: totalSubs,
|
||||
total_views: totalViews,
|
||||
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
|
||||
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
|
||||
avg_post_cost: purchasesCount > 0 ? totalCost / purchasesCount : null,
|
||||
clicks_count: totalSubs, // Using subscriptions as proxy
|
||||
reach_volume: totalViews,
|
||||
total_discounts: 0,
|
||||
avg_discount_percent: 0,
|
||||
avg_conversion: 0,
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.period.localeCompare(b.period));
|
||||
|
||||
// Calculate totals
|
||||
const totals = periods.reduce(
|
||||
(acc, period) => {
|
||||
acc.total_cost += period.metrics.total_cost;
|
||||
acc.purchases_count += period.metrics.purchases_count;
|
||||
acc.total_subscriptions += period.metrics.total_subscriptions;
|
||||
acc.total_views += period.metrics.total_views;
|
||||
acc.clicks_count += period.metrics.clicks_count;
|
||||
acc.reach_volume += period.metrics.reach_volume;
|
||||
acc.total_discounts += period.metrics.total_discounts || 0;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
total_cost: 0,
|
||||
purchases_count: 0,
|
||||
total_subscriptions: 0,
|
||||
total_views: 0,
|
||||
clicks_count: 0,
|
||||
reach_volume: 0,
|
||||
total_discounts: 0,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
periods,
|
||||
totals: {
|
||||
...totals,
|
||||
avg_cpf: totals.total_subscriptions > 0 ? totals.total_cost / totals.total_subscriptions : null,
|
||||
avg_cpm: totals.total_views > 0 ? (totals.total_cost / totals.total_views) * 1000 : null,
|
||||
avg_post_cost: totals.purchases_count > 0 ? totals.total_cost / totals.purchases_count : null,
|
||||
avg_discount_percent: 0,
|
||||
avg_conversion: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
return analyticsApi.projects(workspaceId, params);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
OverviewAnalytics,
|
||||
PlacementAnalyticsItem,
|
||||
} from "@/lib/types/api";
|
||||
import { DEMO_WORKSPACE_ID, DEMO_USER } from "./constants";
|
||||
|
||||
@@ -590,6 +591,85 @@ export const demoChannelAnalytics: ChannelAnalyticsItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Placements Analytics (Projects Analytics)
|
||||
export const demoPlacementAnalytics: PlacementAnalyticsItem[] = [
|
||||
{
|
||||
id: "plac-0001-0000-0000-000000000001",
|
||||
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: "Крипто - Основной",
|
||||
placement_date: "2024-12-20T12:00:00Z",
|
||||
cost: 5500,
|
||||
subscriptions_count: 156,
|
||||
views_count: 8500,
|
||||
cpf: 35.26,
|
||||
cpm: 647.06,
|
||||
},
|
||||
{
|
||||
id: "plac-0002-0000-0000-000000000002",
|
||||
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: "Крипто - Основной",
|
||||
placement_date: "2024-12-18T14:00:00Z",
|
||||
cost: 3200,
|
||||
subscriptions_count: 89,
|
||||
views_count: 4200,
|
||||
cpf: 35.96,
|
||||
cpm: 761.9,
|
||||
},
|
||||
{
|
||||
id: "plac-0003-0000-0000-000000000003",
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
placement_channel_id: "chan-0003-0000-0000-000000000003",
|
||||
placement_channel_title: "Программисты",
|
||||
creative_id: "crea-0001-0000-0000-000000000001",
|
||||
creative_name: "Крипто - Основной",
|
||||
placement_date: "2024-12-15T10:00:00Z",
|
||||
cost: 4800,
|
||||
subscriptions_count: 234,
|
||||
views_count: 12000,
|
||||
cpf: 20.51,
|
||||
cpm: 400.0,
|
||||
},
|
||||
{
|
||||
id: "plac-0004-0000-0000-000000000004",
|
||||
project_id: "proj-0002-0000-0000-000000000002",
|
||||
project_channel_title: "IT Вакансии",
|
||||
placement_channel_id: "chan-0004-0000-0000-000000000004",
|
||||
placement_channel_title: "Маркетинг Pro",
|
||||
creative_id: "crea-0002-0000-0000-000000000002",
|
||||
creative_name: "Крипто - Агрессивный",
|
||||
placement_date: "2024-12-10T16:00:00Z",
|
||||
cost: 2500,
|
||||
subscriptions_count: 67,
|
||||
views_count: 3100,
|
||||
cpf: 37.31,
|
||||
cpm: 806.45,
|
||||
},
|
||||
{
|
||||
id: "plac-0005-0000-0000-000000000005",
|
||||
project_id: "proj-0002-0000-0000-000000000002",
|
||||
project_channel_title: "IT Вакансии",
|
||||
placement_channel_id: "chan-0005-0000-0000-000000000005",
|
||||
placement_channel_title: "Финансовая грамотность",
|
||||
creative_id: "crea-0002-0000-0000-000000000002",
|
||||
creative_name: "Крипто - Агрессивный",
|
||||
placement_date: "2024-12-05T11:00:00Z",
|
||||
cost: 6200,
|
||||
subscriptions_count: 312,
|
||||
views_count: 15600,
|
||||
cpf: 19.87,
|
||||
cpm: 397.44,
|
||||
},
|
||||
];
|
||||
|
||||
export const demoOverviewAnalytics: OverviewAnalytics = {
|
||||
total_cost: { value: 523400.5, delta_percent: 12.4 },
|
||||
total_reach: { value: 380000, delta_percent: -8.1 },
|
||||
|
||||
Reference in New Issue
Block a user