Files
tgex-frontend/lib/demo/demo-aware-api.ts
2026-01-12 12:46:29 +03:00

522 lines
18 KiB
TypeScript

// ============================================================================
// Demo-Aware API Wrapper
// ============================================================================
// 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,
demoCreatives,
demoPlacements,
demoPurchasePlanChannels,
demoSpendingAnalytics,
demoCreativeAnalytics,
demoChannelAnalytics,
demoProjects,
demoWorkspace,
demoMember,
demoOverviewAnalytics,
demoPlacementAnalytics,
} from "./mock-data";
import type {
PaginatedResponse,
Channel,
Creative,
Placement,
PurchasePlanChannel,
SpendingAnalytics,
CreativeAnalyticsItem,
ChannelAnalyticsItem,
Project,
Workspace,
WorkspaceMember,
DateGrouping,
OverviewAnalytics,
PlacementAnalyticsItem,
ProjectsAnalyticsResponse,
ProjectsAnalyticsQueryParams,
} from "@/lib/types/api";
import {
workspacesApi,
projectsApi,
channelsApi,
creativesApi,
placementsApi,
purchasePlanApi,
analyticsApi,
membersApi,
} from "@/lib/api";
// ============================================================================
// Helper
// ============================================================================
const paginate = <T>(items: T[], page = 1, size = 50): PaginatedResponse<T> => ({
items: items.slice((page - 1) * size, page * size),
total: items.length,
page,
size,
pages: Math.ceil(items.length / size),
});
// ============================================================================
// Demo-Aware Workspaces API
// ============================================================================
export const demoAwareWorkspacesApi = {
list: async (params?: { page?: number; size?: number }) => {
// Always include demo workspace in list for demo mode
return workspacesApi.list(params);
},
get: async (workspaceId: string): Promise<Workspace> => {
if (isDemoWorkspace(workspaceId)) {
return demoWorkspace;
}
// Workspaces API doesn't have get method, fetch from list
const response = await workspacesApi.list({ size: 100 });
const workspace = response.items.find(w => w.id === workspaceId);
if (!workspace) throw new Error("Workspace not found");
return workspace;
},
};
// ============================================================================
// Demo-Aware Projects API
// ============================================================================
export const demoAwareProjectsApi = {
list: async (
workspaceId: string,
params?: { page?: number; size?: number; include_archived?: boolean }
): Promise<PaginatedResponse<Project>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoProjects;
if (!params?.include_archived) {
items = items.filter(p => p.status !== "archived");
}
return paginate(items, params?.page, params?.size);
}
return projectsApi.list(workspaceId, params);
},
updateInviteLinkType: async (
workspaceId: string,
projectId: string,
data: { purchase_invite_type_default: "public" | "approval" }
): Promise<Project> => {
if (isDemoWorkspace(workspaceId)) {
// In demo mode, just return the project (mock update)
const project = demoProjects.find(p => p.id === projectId);
if (!project) throw new Error("Project not found");
return { ...project, purchase_invite_type_default: data.purchase_invite_type_default };
}
return projectsApi.updateInviteLinkType(workspaceId, projectId, data);
},
archive: 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: "archived" };
}
return projectsApi.archive(workspaceId, projectId);
},
delete: async (
workspaceId: string,
projectId: string
): Promise<void> => {
if (isDemoWorkspace(workspaceId)) {
// In demo mode, just mark as archived (soft delete)
const project = demoProjects.find(p => p.id === projectId);
if (project) {
project.status = "archived";
}
return;
}
return projectsApi.delete(workspaceId, projectId);
},
};
// ============================================================================
// Demo-Aware Members API
// ============================================================================
export const demoAwareMembersApi = {
list: async (
workspaceId: string,
params?: { page?: number; size?: number }
): Promise<PaginatedResponse<WorkspaceMember>> => {
if (isDemoWorkspace(workspaceId)) {
return paginate([demoMember], params?.page, params?.size);
}
return membersApi.list(workspaceId, params);
},
};
// ============================================================================
// Demo-Aware Channels API
// ============================================================================
export const demoAwareChannelsApi = {
list: async (params?: {
page?: number;
size?: number;
username?: string;
}): Promise<PaginatedResponse<Channel>> => {
// Channels are global, check if we're in demo context via some mechanism
// For now, always return real data - demo channels are handled per-workspace
return channelsApi.list(params);
},
listForDemo: (): PaginatedResponse<Channel> => {
return paginate(demoChannels);
},
};
// ============================================================================
// Demo-Aware Creatives API
// ============================================================================
export const demoAwareCreativesApi = {
list: async (
workspaceId: string,
params?: {
project_id?: string;
include_archived?: boolean;
page?: number;
size?: number;
}
): Promise<PaginatedResponse<Creative>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoCreatives;
if (params?.project_id) {
items = items.filter((c) => c.project_id === params.project_id);
}
if (!params?.include_archived) {
items = items.filter((c) => c.status === "active");
}
return paginate(items, params?.page, params?.size);
}
return creativesApi.list(workspaceId, params);
},
get: async (workspaceId: string, creativeId: string): Promise<Creative> => {
if (isDemoWorkspace(workspaceId)) {
const creative = demoCreatives.find((c) => c.id === creativeId);
if (!creative) throw new Error("Creative not found");
return creative;
}
return creativesApi.get(workspaceId, creativeId);
},
};
// ============================================================================
// Demo-Aware Placements API
// ============================================================================
export const demoAwarePlacementsApi = {
list: async (
workspaceId: string,
params?: {
project_id?: string;
placement_channel_id?: string;
creative_id?: string;
include_archived?: boolean;
page?: number;
size?: number;
}
): Promise<PaginatedResponse<Placement>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoPlacements;
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?.include_archived) {
items = items.filter((p) => p.status === "active");
}
return paginate(items, params?.page, params?.size);
}
return placementsApi.list(workspaceId, params);
},
get: async (workspaceId: string, placementId: string): Promise<Placement> => {
if (isDemoWorkspace(workspaceId)) {
const placement = demoPlacements.find((p) => p.id === placementId);
if (!placement) throw new Error("Placement not found");
return placement;
}
return placementsApi.get(workspaceId, placementId);
},
};
// ============================================================================
// Demo-Aware Purchase Plan API
// ============================================================================
export const demoAwarePurchasePlanApi = {
list: async (
workspaceId: string,
projectId: string,
params?: { page?: number; size?: number }
): Promise<PaginatedResponse<PurchasePlanChannel>> => {
if (isDemoWorkspace(workspaceId)) {
const items = demoPurchasePlanChannels[projectId] || [];
return paginate(items, params?.page, params?.size);
}
return purchasePlanApi.list(workspaceId, projectId, params);
},
};
// ============================================================================
// Demo-Aware Analytics API
// ============================================================================
export const demoAwareAnalyticsApi = {
spending: async (
workspaceId: string,
params?: {
project_id?: string;
date_from?: string;
date_to?: string;
grouping?: DateGrouping;
}
): Promise<SpendingAnalytics> => {
if (isDemoWorkspace(workspaceId)) {
if (params?.project_id) {
const projectPlacements = demoPlacements.filter(
(p) => p.project_id === params.project_id && p.status === "active"
);
const totalCost = projectPlacements.reduce(
(sum, p) => sum + (p.cost || 0),
0
);
const totalSubs = projectPlacements.reduce(
(sum, p) => sum + (p.subscriptions_count || 0),
0
);
const totalViews = projectPlacements.reduce(
(sum, p) => sum + (p.views_count || 0),
0
);
return {
total_cost: totalCost,
total_subscriptions: totalSubs,
total_views: totalViews,
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
chart_data: demoSpendingAnalytics.chart_data.slice(0, 4),
};
}
return demoSpendingAnalytics;
}
return analyticsApi.spending(workspaceId, params);
},
creatives: async (
workspaceId: string,
params?: { project_id?: string; page?: number; size?: number }
): Promise<PaginatedResponse<CreativeAnalyticsItem>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoCreativeAnalytics;
if (params?.project_id) {
const projectCreativeIds = demoCreatives
.filter((c) => c.project_id === params.project_id)
.map((c) => c.id);
items = items.filter((a) => projectCreativeIds.includes(a.id));
}
return paginate(items, params?.page, params?.size);
}
return analyticsApi.creatives(workspaceId, params);
},
channels: async (
workspaceId: string,
params?: { project_id?: string; page?: number; size?: number }
): Promise<PaginatedResponse<ChannelAnalyticsItem>> => {
if (isDemoWorkspace(workspaceId)) {
let items = demoChannelAnalytics;
if (params?.project_id) {
const projectPlacementChannelIds = new Set(
demoPlacements
.filter((p) => p.project_id === params.project_id)
.map((p) => p.placement_channel_id)
);
items = items.filter((a) =>
projectPlacementChannelIds.has(a.channel_id)
);
}
return paginate(items, params?.page, params?.size);
}
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 }
): Promise<OverviewAnalytics> => {
if (isDemoWorkspace(workspaceId)) {
return demoOverviewAnalytics;
}
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);
},
};