// ============================================================================ // 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, demoPlacementChannels, 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 = (items: T[], page = 1, size = 50): PaginatedResponse => ({ 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 => { 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> => { 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 => { 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 => { 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); }, unarchive: async ( workspaceId: string, projectId: string ): Promise => { 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 ): Promise => { 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); }, move: async ( workspaceId: string, projectId: string, data: { target_workspace_id: string } ): Promise => { 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); }, }; // ============================================================================ // Demo-Aware Members API // ============================================================================ export const demoAwareMembersApi = { list: async ( workspaceId: string, params?: { page?: number; size?: number } ): Promise> => { 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> => { // 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 => { 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> => { 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 => { 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> => { 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.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 !== "Отмена" && p.status !== "Неактуально"); } return paginate(items, params?.page, params?.size); } return placementsApi.list(workspaceId, params); }, get: async (workspaceId: string, placementId: string, _projectId?: string): Promise => { 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, _projectId); }, getByProject: async (workspaceId: string, projectId: string): Promise => { 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); }, }; // ============================================================================ // Demo-Aware Purchase Plan API // ============================================================================ export const demoAwarePurchasePlanApi = { list: async ( workspaceId: string, projectId: string, params?: { page?: number; size?: number } ): Promise> => { 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 => { if (isDemoWorkspace(workspaceId)) { if (params?.project_id) { const projectPlacements = demoPlacements.filter( (p) => p.project_id === params.project_id && p.status !== "Отмена" && p.status !== "Неактуально" ); const totalCost = projectPlacements.reduce( (sum, p) => sum + (p.details?.cost?.value || 0), 0 ); const totalSubs = projectPlacements.reduce( (sum, p) => sum + (p.placement_post?.subscriptions_count || 0), 0 ); const totalViews = projectPlacements.reduce( (sum, p) => sum + (p.placement_post?.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), placements_count: projectPlacements.length, }; } return demoSpendingAnalytics; } return analyticsApi.spending(workspaceId, params); }, creatives: async ( workspaceId: string, params?: { project_id?: string; page?: number; size?: number } ): Promise> => { 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> => { 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.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_ids?: string; status_list?: string; placement_channel_ids?: string; creative_ids?: string; date_from?: string; date_to?: string; page?: number; size?: number; } ): Promise> => { if (isDemoWorkspace(workspaceId)) { let items = demoPlacementAnalytics; if (params?.project_ids) { items = items.filter((p) => p.project_id === params.project_ids); } if (params?.placement_channel_ids) { items = items.filter((p) => params.placement_channel_ids!.includes(p.channel_id)); } if (params?.creative_ids) { items = items.filter((p) => p.creative_id && params.creative_ids!.includes(p.creative_id)); } if (params?.date_from) { const from = new Date(params.date_from); items = items.filter((p) => p.placement_date && new Date(p.placement_date) >= from); } if (params?.date_to) { const to = new Date(params.date_to); items = items.filter((p) => p.placement_date && new Date(p.placement_date) <= to); } 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 => { if (isDemoWorkspace(workspaceId)) { return demoOverviewAnalytics; } return analyticsApi.overview(workspaceId, params); }, projects: async ( workspaceId: string, params: ProjectsAnalyticsQueryParams ): Promise => { 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) => p.placement_date && new Date(p.placement_date) >= from); } if (params.date_to) { const to = new Date(params.date_to); filtered = filtered.filter((p) => p.placement_date && 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 = {}; filtered.forEach((item) => { if (!item.placement_date) return; 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); }, };