feat: all project setup
This commit is contained in:
277
lib/mocks/data/analytics.ts
Normal file
277
lib/mocks/data/analytics.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import type {
|
||||
AnalyticsOverview,
|
||||
CostsReport,
|
||||
AnalyticsChannelData,
|
||||
AnalyticsCreativeData,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Analytics Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockAnalyticsOverview: AnalyticsOverview = {
|
||||
totalPurchases: 25,
|
||||
purchasesChange: 5,
|
||||
totalFollowers: 2500,
|
||||
followersChange: 320,
|
||||
averageCpf: 50.0,
|
||||
cpfChange: -5.2,
|
||||
averageCpm: 450.0,
|
||||
cpmChange: -3.8,
|
||||
topChannelsByCpf: [
|
||||
{
|
||||
channel_id: "ec3",
|
||||
channel_name: "IT и Технологии",
|
||||
total_purchases: 3,
|
||||
avg_cpf: 38.2,
|
||||
},
|
||||
{
|
||||
channel_id: "ec1",
|
||||
channel_name: "Канал о Маркетинге",
|
||||
total_purchases: 5,
|
||||
avg_cpf: 40.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec5",
|
||||
channel_name: "Продажи и Переговоры",
|
||||
total_purchases: 4,
|
||||
avg_cpf: 45.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec2",
|
||||
channel_name: "Бизнес и Стартапы",
|
||||
total_purchases: 8,
|
||||
avg_cpf: 55.8,
|
||||
},
|
||||
{
|
||||
channel_id: "ec4",
|
||||
channel_name: "Крипто Инвестиции",
|
||||
total_purchases: 2,
|
||||
avg_cpf: 68.5,
|
||||
},
|
||||
],
|
||||
topCreativesByCpf: [
|
||||
{
|
||||
creative_id: "cr1",
|
||||
creative_name: 'Креатив "Присоединяйся"',
|
||||
total_subscriptions: 1180,
|
||||
avg_cpf: 42.4,
|
||||
},
|
||||
{
|
||||
creative_id: "cr2",
|
||||
creative_name: 'Креатив "Эксклюзив"',
|
||||
total_subscriptions: 577,
|
||||
avg_cpf: 48.5,
|
||||
},
|
||||
{
|
||||
creative_id: "cr3",
|
||||
creative_name: 'Креатив "Обучение"',
|
||||
total_subscriptions: 803,
|
||||
avg_cpf: 52.3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Mock data for costs report
|
||||
export const mockCostsReportByDay: CostsReport = {
|
||||
totalCost: 76500,
|
||||
averageCost: 7650,
|
||||
periods: [
|
||||
{
|
||||
date: "2024-02-20",
|
||||
total_cost: 5000,
|
||||
purchases_count: 1,
|
||||
avg_cost: 5000,
|
||||
},
|
||||
{
|
||||
date: "2024-02-22",
|
||||
total_cost: 3500,
|
||||
purchases_count: 1,
|
||||
avg_cost: 3500,
|
||||
},
|
||||
{
|
||||
date: "2024-02-25",
|
||||
total_cost: 7000,
|
||||
purchases_count: 2,
|
||||
avg_cost: 3500,
|
||||
},
|
||||
{
|
||||
date: "2024-02-28",
|
||||
total_cost: 4500,
|
||||
purchases_count: 1,
|
||||
avg_cost: 4500,
|
||||
},
|
||||
{
|
||||
date: "2024-03-02",
|
||||
total_cost: 8500,
|
||||
purchases_count: 2,
|
||||
avg_cost: 4250,
|
||||
},
|
||||
{
|
||||
date: "2024-03-05",
|
||||
total_cost: 6000,
|
||||
purchases_count: 1,
|
||||
avg_cost: 6000,
|
||||
},
|
||||
{
|
||||
date: "2024-03-08",
|
||||
total_cost: 12000,
|
||||
purchases_count: 3,
|
||||
avg_cost: 4000,
|
||||
},
|
||||
{
|
||||
date: "2024-03-12",
|
||||
total_cost: 5500,
|
||||
purchases_count: 1,
|
||||
avg_cost: 5500,
|
||||
},
|
||||
{
|
||||
date: "2024-03-15",
|
||||
total_cost: 15000,
|
||||
purchases_count: 3,
|
||||
avg_cost: 5000,
|
||||
},
|
||||
{
|
||||
date: "2024-03-18",
|
||||
total_cost: 9500,
|
||||
purchases_count: 2,
|
||||
avg_cost: 4750,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockCostsReportByWeek: CostsReport = {
|
||||
totalCost: 125000,
|
||||
averageCost: 15625,
|
||||
periods: [
|
||||
{
|
||||
date: "2024-02-19",
|
||||
total_cost: 15500,
|
||||
purchases_count: 4,
|
||||
avg_cost: 3875,
|
||||
},
|
||||
{
|
||||
date: "2024-02-26",
|
||||
total_cost: 13000,
|
||||
purchases_count: 3,
|
||||
avg_cost: 4333,
|
||||
},
|
||||
{
|
||||
date: "2024-03-04",
|
||||
total_cost: 26500,
|
||||
purchases_count: 6,
|
||||
avg_cost: 4417,
|
||||
},
|
||||
{
|
||||
date: "2024-03-11",
|
||||
total_cost: 33000,
|
||||
purchases_count: 7,
|
||||
avg_cost: 4714,
|
||||
},
|
||||
{
|
||||
date: "2024-03-18",
|
||||
total_cost: 24500,
|
||||
purchases_count: 5,
|
||||
avg_cost: 4900,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const mockCostsReportByMonth: CostsReport = {
|
||||
totalCost: 125000,
|
||||
averageCost: 62500,
|
||||
periods: [
|
||||
{
|
||||
date: "2024-02-01",
|
||||
total_cost: 20000,
|
||||
purchases_count: 5,
|
||||
avg_cost: 4000,
|
||||
},
|
||||
{
|
||||
date: "2024-03-01",
|
||||
total_cost: 105000,
|
||||
purchases_count: 20,
|
||||
avg_cost: 5250,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Mock data for channels analytics
|
||||
export const mockAnalyticsChannelsData: AnalyticsChannelData[] = [
|
||||
{
|
||||
channel_id: "ec1",
|
||||
channel_name: "Канал о Маркетинге",
|
||||
purchases_count: 5,
|
||||
total_cost: 25000,
|
||||
total_subscriptions: 625,
|
||||
avg_cpf: 40.0,
|
||||
avg_cpm: 380.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec2",
|
||||
channel_name: "Бизнес и Стартапы",
|
||||
purchases_count: 8,
|
||||
total_cost: 48000,
|
||||
total_subscriptions: 860,
|
||||
avg_cpf: 55.8,
|
||||
avg_cpm: 450.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec3",
|
||||
channel_name: "IT и Технологии",
|
||||
purchases_count: 3,
|
||||
total_cost: 12000,
|
||||
total_subscriptions: 314,
|
||||
avg_cpf: 38.2,
|
||||
avg_cpm: 360.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec4",
|
||||
channel_name: "Крипто Инвестиции",
|
||||
purchases_count: 2,
|
||||
total_cost: 18000,
|
||||
total_subscriptions: 263,
|
||||
avg_cpf: 68.5,
|
||||
avg_cpm: 520.0,
|
||||
},
|
||||
{
|
||||
channel_id: "ec5",
|
||||
channel_name: "Продажи и Переговоры",
|
||||
purchases_count: 4,
|
||||
total_cost: 16000,
|
||||
total_subscriptions: 356,
|
||||
avg_cpf: 45.0,
|
||||
avg_cpm: 410.0,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock data for creatives analytics
|
||||
export const mockAnalyticsCreativesData: AnalyticsCreativeData[] = [
|
||||
{
|
||||
creative_id: "cr1",
|
||||
creative_name: 'Креатив "Присоединяйся"',
|
||||
purchases_count: 10,
|
||||
total_cost: 50000,
|
||||
total_subscriptions: 1180,
|
||||
avg_cpf: 42.4,
|
||||
conversion_rate: 0.95,
|
||||
},
|
||||
{
|
||||
creative_id: "cr2",
|
||||
creative_name: 'Креатив "Эксклюзив"',
|
||||
purchases_count: 5,
|
||||
total_cost: 28000,
|
||||
total_subscriptions: 577,
|
||||
avg_cpf: 48.5,
|
||||
conversion_rate: 0.88,
|
||||
},
|
||||
{
|
||||
creative_id: "cr3",
|
||||
creative_name: 'Креатив "Обучение"',
|
||||
purchases_count: 8,
|
||||
total_cost: 42000,
|
||||
total_subscriptions: 803,
|
||||
avg_cpf: 52.3,
|
||||
conversion_rate: 0.92,
|
||||
},
|
||||
];
|
||||
79
lib/mocks/data/channels.ts
Normal file
79
lib/mocks/data/channels.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { TargetChannel, TargetChannelDetail } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Target Channels Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockTargetChannels: TargetChannel[] = [
|
||||
{
|
||||
id: "tc1",
|
||||
telegram_id: -1001234567890,
|
||||
title: "Мой Главный Канал",
|
||||
username: "my_main_channel",
|
||||
description: "Основной канал для продвижения продукта",
|
||||
is_active: true,
|
||||
created_at: "2024-01-10T10:00:00.000Z",
|
||||
updated_at: "2024-01-10T10:00:00.000Z",
|
||||
total_purchases: 15,
|
||||
total_subscriptions: 1250,
|
||||
avg_cpf: 45.5,
|
||||
},
|
||||
{
|
||||
id: "tc2",
|
||||
telegram_id: -1009876543210,
|
||||
title: "Тестовый Канал",
|
||||
username: "test_channel_promo",
|
||||
description: "Канал для тестирования рекламных креативов",
|
||||
is_active: true,
|
||||
created_at: "2024-02-15T12:00:00.000Z",
|
||||
updated_at: "2024-02-15T12:00:00.000Z",
|
||||
total_purchases: 8,
|
||||
total_subscriptions: 450,
|
||||
avg_cpf: 52.3,
|
||||
},
|
||||
{
|
||||
id: "tc3",
|
||||
telegram_id: -1005555555555,
|
||||
title: "Новости и Обновления",
|
||||
username: null,
|
||||
description: "Приватный канал для новостей",
|
||||
is_active: false,
|
||||
created_at: "2024-03-01T08:00:00.000Z",
|
||||
updated_at: "2024-03-20T14:30:00.000Z",
|
||||
total_purchases: 3,
|
||||
total_subscriptions: 120,
|
||||
avg_cpf: 67.8,
|
||||
},
|
||||
];
|
||||
|
||||
export const getMockTargetChannelDetail = (
|
||||
id: string
|
||||
): TargetChannelDetail | null => {
|
||||
const channel = mockTargetChannels.find((c) => c.id === id);
|
||||
if (!channel) return null;
|
||||
|
||||
return {
|
||||
...channel,
|
||||
recent_purchases: [], // Will be populated from purchases mock
|
||||
stats_by_period: [
|
||||
{
|
||||
period: "day",
|
||||
subscriptions: 25,
|
||||
cost: 1200,
|
||||
cpf: 48.0,
|
||||
},
|
||||
{
|
||||
period: "week",
|
||||
subscriptions: 180,
|
||||
cost: 8500,
|
||||
cpf: 47.2,
|
||||
},
|
||||
{
|
||||
period: "month",
|
||||
subscriptions: 750,
|
||||
cost: 35000,
|
||||
cpf: 46.7,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
71
lib/mocks/data/creatives.ts
Normal file
71
lib/mocks/data/creatives.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Creative, CreativeDetail } from "@/lib/types/api";
|
||||
import { mockTargetChannels } from "./channels";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Creatives Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockCreatives: Creative[] = [
|
||||
{
|
||||
id: "cr1",
|
||||
name: 'Креатив "Присоединяйся"',
|
||||
text: "🔥 Присоединяйся к нашему сообществу!\n\nУзнавай первым о новых возможностях и фишках.\n\n👉 {link}",
|
||||
target_channel_id: "tc1",
|
||||
is_archived: false,
|
||||
created_at: "2024-01-15T10:00:00.000Z",
|
||||
updated_at: "2024-01-15T10:00:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
total_purchases: 10,
|
||||
total_subscriptions: 850,
|
||||
avg_cpf: 42.3,
|
||||
},
|
||||
{
|
||||
id: "cr2",
|
||||
name: 'Креатив "Эксклюзив"',
|
||||
text: "⭐ Эксклюзивный контент только для подписчиков!\n\nНе упусти возможность быть в курсе всех трендов.\n\nПереходи: {link}",
|
||||
target_channel_id: "tc1",
|
||||
is_archived: false,
|
||||
created_at: "2024-01-20T12:00:00.000Z",
|
||||
updated_at: "2024-01-20T12:00:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
total_purchases: 5,
|
||||
total_subscriptions: 400,
|
||||
avg_cpf: 48.5,
|
||||
},
|
||||
{
|
||||
id: "cr3",
|
||||
name: 'Креатив "Обучение"',
|
||||
text: "📚 Хочешь научиться зарабатывать больше?\n\nПодписывайся на канал с проверенными методиками!\n\n{link}",
|
||||
target_channel_id: "tc2",
|
||||
is_archived: false,
|
||||
created_at: "2024-02-05T09:00:00.000Z",
|
||||
updated_at: "2024-02-05T09:00:00.000Z",
|
||||
target_channel: mockTargetChannels[1],
|
||||
total_purchases: 8,
|
||||
total_subscriptions: 450,
|
||||
avg_cpf: 52.3,
|
||||
},
|
||||
{
|
||||
id: "cr4",
|
||||
name: "Старый креатив (архив)",
|
||||
text: "Устаревший текст с предложением. {link}",
|
||||
target_channel_id: "tc1",
|
||||
is_archived: true,
|
||||
created_at: "2023-12-01T10:00:00.000Z",
|
||||
updated_at: "2024-01-10T15:00:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
total_purchases: 2,
|
||||
total_subscriptions: 80,
|
||||
avg_cpf: 75.0,
|
||||
},
|
||||
];
|
||||
|
||||
export const getMockCreativeDetail = (id: string): CreativeDetail | null => {
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) return null;
|
||||
|
||||
return {
|
||||
...creative,
|
||||
purchases: [], // Will be populated from purchases mock
|
||||
};
|
||||
};
|
||||
95
lib/mocks/data/external-channels.ts
Normal file
95
lib/mocks/data/external-channels.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ExternalChannel, ExternalChannelDetail } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Channels Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockExternalChannels: ExternalChannel[] = [
|
||||
{
|
||||
id: "ec1",
|
||||
telegram_id: -1001111111111,
|
||||
title: "Канал о Маркетинге",
|
||||
username: "marketing_pro",
|
||||
link: "https://t.me/marketing_pro",
|
||||
subscribers_count: 50000,
|
||||
description: "Лучшие практики маркетинга",
|
||||
created_at: "2024-01-05T10:00:00.000Z",
|
||||
updated_at: "2024-01-05T10:00:00.000Z",
|
||||
target_channels: ["tc1", "tc2"],
|
||||
total_purchases: 5,
|
||||
avg_cpf: 42.5,
|
||||
avg_cpm: 850.0,
|
||||
},
|
||||
{
|
||||
id: "ec2",
|
||||
telegram_id: -1002222222222,
|
||||
title: "Бизнес и Стартапы",
|
||||
username: "business_startups",
|
||||
link: "https://t.me/business_startups",
|
||||
subscribers_count: 120000,
|
||||
description: "Новости мира бизнеса",
|
||||
created_at: "2024-01-08T11:00:00.000Z",
|
||||
updated_at: "2024-01-08T11:00:00.000Z",
|
||||
target_channels: ["tc1"],
|
||||
total_purchases: 8,
|
||||
avg_cpf: 55.8,
|
||||
avg_cpm: 1200.0,
|
||||
},
|
||||
{
|
||||
id: "ec3",
|
||||
telegram_id: -1003333333333,
|
||||
title: "IT и Технологии",
|
||||
username: "it_tech_news",
|
||||
link: "https://t.me/it_tech_news",
|
||||
subscribers_count: 85000,
|
||||
description: "Все о новых технологиях",
|
||||
created_at: "2024-01-12T09:00:00.000Z",
|
||||
updated_at: "2024-01-12T09:00:00.000Z",
|
||||
target_channels: ["tc1", "tc2"],
|
||||
total_purchases: 3,
|
||||
avg_cpf: 38.2,
|
||||
avg_cpm: 750.0,
|
||||
},
|
||||
{
|
||||
id: "ec4",
|
||||
telegram_id: null,
|
||||
title: "Крипто Инвестиции",
|
||||
username: "crypto_invest",
|
||||
link: "https://t.me/crypto_invest",
|
||||
subscribers_count: 200000,
|
||||
description: null,
|
||||
created_at: "2024-02-01T10:00:00.000Z",
|
||||
updated_at: "2024-02-01T10:00:00.000Z",
|
||||
target_channels: ["tc2"],
|
||||
total_purchases: 2,
|
||||
avg_cpf: 68.5,
|
||||
avg_cpm: 1500.0,
|
||||
},
|
||||
{
|
||||
id: "ec5",
|
||||
telegram_id: -1005555555555,
|
||||
title: "Продажи и Переговоры",
|
||||
username: "sales_expert",
|
||||
link: "https://t.me/sales_expert",
|
||||
subscribers_count: 35000,
|
||||
description: "Экспертиза в продажах",
|
||||
created_at: "2024-02-10T14:00:00.000Z",
|
||||
updated_at: "2024-02-10T14:00:00.000Z",
|
||||
target_channels: ["tc1"],
|
||||
total_purchases: 4,
|
||||
avg_cpf: 45.0,
|
||||
avg_cpm: 900.0,
|
||||
},
|
||||
];
|
||||
|
||||
export const getMockExternalChannelDetail = (
|
||||
id: string
|
||||
): ExternalChannelDetail | null => {
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) return null;
|
||||
|
||||
return {
|
||||
...channel,
|
||||
purchases: [], // Will be populated from purchases mock
|
||||
};
|
||||
};
|
||||
264
lib/mocks/data/purchases.ts
Normal file
264
lib/mocks/data/purchases.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
Purchase,
|
||||
PurchaseDetail,
|
||||
Subscription,
|
||||
ViewsSnapshot,
|
||||
} from "@/lib/types/api";
|
||||
import { mockTargetChannels } from "./channels";
|
||||
import { mockExternalChannels } from "./external-channels";
|
||||
import { mockCreatives } from "./creatives";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Purchases Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockPurchases: Purchase[] = [
|
||||
{
|
||||
id: "p1",
|
||||
target_channel_id: "tc1",
|
||||
external_channel_id: "ec1",
|
||||
creative_id: "cr1",
|
||||
scheduled_date: "2024-03-15T10:00:00.000Z",
|
||||
actual_date: "2024-03-15T11:30:00.000Z",
|
||||
cost: 5000,
|
||||
comment: "Тестовая закупка на утро",
|
||||
post_link: "https://t.me/marketing_pro/1234",
|
||||
invite_link: "https://t.me/+AbCdEfGhIjKlMnOp",
|
||||
invite_link_type: "public",
|
||||
is_archived: false,
|
||||
created_at: "2024-03-14T15:00:00.000Z",
|
||||
updated_at: "2024-03-15T11:30:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
external_channel: mockExternalChannels[0],
|
||||
creative: mockCreatives[0],
|
||||
subscriptions_count: 125,
|
||||
views_count: 12500,
|
||||
cpf: 40.0,
|
||||
cpm: 400.0,
|
||||
conversion_rate: 1.0,
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
target_channel_id: "tc1",
|
||||
external_channel_id: "ec2",
|
||||
creative_id: "cr2",
|
||||
scheduled_date: "2024-03-18T14:00:00.000Z",
|
||||
actual_date: "2024-03-18T14:15:00.000Z",
|
||||
cost: 12000,
|
||||
comment: "Большой канал, ожидаем хороших результатов",
|
||||
post_link: "https://t.me/business_startups/5678",
|
||||
invite_link: "https://t.me/+QrStUvWxYzAbCdEf",
|
||||
invite_link_type: "public",
|
||||
is_archived: false,
|
||||
created_at: "2024-03-17T10:00:00.000Z",
|
||||
updated_at: "2024-03-18T14:15:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
external_channel: mockExternalChannels[1],
|
||||
creative: mockCreatives[1],
|
||||
subscriptions_count: 210,
|
||||
views_count: 28000,
|
||||
cpf: 57.1,
|
||||
cpm: 428.6,
|
||||
conversion_rate: 0.75,
|
||||
},
|
||||
{
|
||||
id: "p3",
|
||||
target_channel_id: "tc2",
|
||||
external_channel_id: "ec3",
|
||||
creative_id: "cr3",
|
||||
scheduled_date: "2024-03-20T16:00:00.000Z",
|
||||
actual_date: null,
|
||||
cost: 3500,
|
||||
comment: null,
|
||||
post_link: "https://t.me/it_tech_news/9101",
|
||||
invite_link: "https://t.me/+GhIjKlMnOpQrStUv",
|
||||
invite_link_type: "private",
|
||||
is_archived: false,
|
||||
created_at: "2024-03-19T12:00:00.000Z",
|
||||
updated_at: "2024-03-19T12:00:00.000Z",
|
||||
target_channel: mockTargetChannels[1],
|
||||
external_channel: mockExternalChannels[2],
|
||||
creative: mockCreatives[2],
|
||||
subscriptions_count: 85,
|
||||
views_count: 8500,
|
||||
cpf: 41.2,
|
||||
cpm: 411.8,
|
||||
conversion_rate: 1.0,
|
||||
},
|
||||
{
|
||||
id: "p4",
|
||||
target_channel_id: "tc1",
|
||||
external_channel_id: "ec5",
|
||||
creative_id: "cr1",
|
||||
scheduled_date: "2024-03-22T12:00:00.000Z",
|
||||
actual_date: null,
|
||||
cost: 4500,
|
||||
comment: "Повторная закупка в том же канале",
|
||||
post_link: null,
|
||||
invite_link: "https://t.me/+WxYzAbCdEfGhIjKl",
|
||||
invite_link_type: "public",
|
||||
is_archived: false,
|
||||
created_at: "2024-03-21T09:00:00.000Z",
|
||||
updated_at: "2024-03-21T09:00:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
external_channel: mockExternalChannels[4],
|
||||
creative: mockCreatives[0],
|
||||
subscriptions_count: 95,
|
||||
views_count: null,
|
||||
cpf: 47.4,
|
||||
cpm: null,
|
||||
conversion_rate: null,
|
||||
},
|
||||
{
|
||||
id: "p5",
|
||||
target_channel_id: "tc1",
|
||||
external_channel_id: "ec1",
|
||||
creative_id: "cr1",
|
||||
scheduled_date: "2024-02-10T10:00:00.000Z",
|
||||
actual_date: "2024-02-10T10:30:00.000Z",
|
||||
cost: 5500,
|
||||
comment: "Старая закупка для истории",
|
||||
post_link: "https://t.me/marketing_pro/1100",
|
||||
invite_link: "https://t.me/+OldInviteLink123",
|
||||
invite_link_type: "public",
|
||||
is_archived: true,
|
||||
created_at: "2024-02-09T15:00:00.000Z",
|
||||
updated_at: "2024-02-20T10:00:00.000Z",
|
||||
target_channel: mockTargetChannels[0],
|
||||
external_channel: mockExternalChannels[0],
|
||||
creative: mockCreatives[0],
|
||||
subscriptions_count: 140,
|
||||
views_count: 15000,
|
||||
cpf: 39.3,
|
||||
cpm: 366.7,
|
||||
conversion_rate: 0.93,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock Subscriptions
|
||||
export const mockSubscriptions: Subscription[] = [
|
||||
{
|
||||
id: "s1",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 111111111,
|
||||
user_first_name: "Иван",
|
||||
user_last_name: "Петров",
|
||||
user_username: "user1",
|
||||
subscribed_at: "2024-03-15T11:35:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "chat_member",
|
||||
},
|
||||
{
|
||||
id: "s2",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 222222222,
|
||||
user_first_name: "Мария",
|
||||
user_last_name: "Сидорова",
|
||||
user_username: "user2",
|
||||
subscribed_at: "2024-03-15T11:40:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "chat_member",
|
||||
},
|
||||
{
|
||||
id: "s3",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 333333333,
|
||||
user_first_name: "Алексей",
|
||||
user_last_name: null,
|
||||
user_username: null,
|
||||
subscribed_at: "2024-03-15T12:00:00.000Z",
|
||||
is_active: false,
|
||||
event_type: "join_request",
|
||||
},
|
||||
{
|
||||
id: "s4",
|
||||
purchase_id: "p2",
|
||||
telegram_user_id: 444444444,
|
||||
user_first_name: "Елена",
|
||||
user_last_name: "Кузнецова",
|
||||
user_username: "user4",
|
||||
subscribed_at: "2024-03-18T14:20:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "chat_member",
|
||||
},
|
||||
{
|
||||
id: "s5",
|
||||
purchase_id: "p2",
|
||||
telegram_user_id: 555555555,
|
||||
user_first_name: "Дмитрий",
|
||||
user_last_name: "Смирнов",
|
||||
user_username: "user5",
|
||||
subscribed_at: "2024-03-18T14:25:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "chat_member",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock Views Snapshots
|
||||
export const mockViewsSnapshots: ViewsSnapshot[] = [
|
||||
{
|
||||
id: "v1",
|
||||
purchase_id: "p1",
|
||||
views: 5000,
|
||||
fetched_at: "2024-03-15T12:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v2",
|
||||
purchase_id: "p1",
|
||||
views: 8500,
|
||||
fetched_at: "2024-03-15T18:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v3",
|
||||
purchase_id: "p1",
|
||||
views: 10200,
|
||||
fetched_at: "2024-03-16T09:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v4",
|
||||
purchase_id: "p1",
|
||||
views: 11800,
|
||||
fetched_at: "2024-03-16T18:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v5",
|
||||
purchase_id: "p1",
|
||||
views: 12500,
|
||||
fetched_at: "2024-03-17T12:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v6",
|
||||
purchase_id: "p2",
|
||||
views: 15000,
|
||||
fetched_at: "2024-03-18T15:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v7",
|
||||
purchase_id: "p2",
|
||||
views: 22000,
|
||||
fetched_at: "2024-03-18T20:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v8",
|
||||
purchase_id: "p2",
|
||||
views: 26500,
|
||||
fetched_at: "2024-03-19T09:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v9",
|
||||
purchase_id: "p2",
|
||||
views: 28000,
|
||||
fetched_at: "2024-03-19T18:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
export const getMockPurchaseDetail = (id: string): PurchaseDetail | null => {
|
||||
const purchase = mockPurchases.find((p) => p.id === id);
|
||||
if (!purchase) return null;
|
||||
|
||||
return {
|
||||
...purchase,
|
||||
subscriptions: mockSubscriptions.filter((s) => s.purchase_id === id),
|
||||
views_history: mockViewsSnapshots.filter((v) => v.purchase_id === id),
|
||||
};
|
||||
};
|
||||
32
lib/mocks/data/users.ts
Normal file
32
lib/mocks/data/users.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { User } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Users Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockUsers: User[] = [
|
||||
{
|
||||
id: "1",
|
||||
telegram_id: 123456789,
|
||||
username: "ivan_test",
|
||||
first_name: "Иван",
|
||||
last_name: "Иванов",
|
||||
created_at: "2024-01-15T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
telegram_id: 987654321,
|
||||
username: "maria_test",
|
||||
first_name: "Мария",
|
||||
last_name: "Петрова",
|
||||
created_at: "2024-02-01T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCurrentUser = mockUsers[0];
|
||||
|
||||
// Mock tokens
|
||||
export const mockTokens = {
|
||||
access_token: "mock_access_token_123456789",
|
||||
refresh_token: "mock_refresh_token_123456789",
|
||||
};
|
||||
636
lib/mocks/handlers.ts
Normal file
636
lib/mocks/handlers.ts
Normal file
@@ -0,0 +1,636 @@
|
||||
// ============================================================================
|
||||
// Mock API Handlers
|
||||
// ============================================================================
|
||||
|
||||
import {
|
||||
mockCurrentUser,
|
||||
mockTokens,
|
||||
mockTargetChannels,
|
||||
getMockTargetChannelDetail,
|
||||
mockExternalChannels,
|
||||
getMockExternalChannelDetail,
|
||||
mockCreatives,
|
||||
getMockCreativeDetail,
|
||||
mockPurchases,
|
||||
getMockPurchaseDetail,
|
||||
mockAnalyticsOverview,
|
||||
mockCostsReportByDay,
|
||||
mockCostsReportByWeek,
|
||||
mockCostsReportByMonth,
|
||||
mockAnalyticsChannelsData,
|
||||
mockAnalyticsCreativesData,
|
||||
} from "./index";
|
||||
|
||||
import type {
|
||||
ListResponse,
|
||||
SuccessResponse,
|
||||
ApiError,
|
||||
TargetChannel,
|
||||
ExternalChannel,
|
||||
Creative,
|
||||
Purchase,
|
||||
PurchaseCreateRequest,
|
||||
PurchaseCreateResponse,
|
||||
CreativeCreateRequest,
|
||||
ExternalChannelCreateRequest,
|
||||
PurchaseRefreshViewsResponse,
|
||||
ExternalChannelImportResponse,
|
||||
AuthCompleteRequest,
|
||||
AuthCompleteResponse,
|
||||
AuthRefreshRequest,
|
||||
AuthRefreshResponse,
|
||||
AuthInitResponse,
|
||||
TargetChannelUpdateRequest,
|
||||
ExternalChannelUpdateRequest,
|
||||
CreativeUpdateRequest,
|
||||
PurchaseUpdateRequest,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
// Simulate network delay
|
||||
const delay = (ms: number = 300) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Helper to generate mock error
|
||||
const mockError = (code: string, message: string): ApiError => ({
|
||||
error: { code, message },
|
||||
});
|
||||
|
||||
// Helper to generate IDs
|
||||
let idCounter = 1000;
|
||||
const generateId = () => `mock_${idCounter++}`;
|
||||
|
||||
// ============================================================================
|
||||
// Mock API Handlers
|
||||
// ============================================================================
|
||||
|
||||
export const mockApiHandlers = {
|
||||
// --------------------------------------------------------------------------
|
||||
// Auth
|
||||
// --------------------------------------------------------------------------
|
||||
async authInit(): Promise<AuthInitResponse> {
|
||||
await delay();
|
||||
return {
|
||||
bot_username: "tgex_bot",
|
||||
start_param: "login",
|
||||
};
|
||||
},
|
||||
|
||||
async authComplete(data: AuthCompleteRequest): Promise<AuthCompleteResponse> {
|
||||
await delay();
|
||||
if (!data.token) {
|
||||
throw mockError("VALIDATION_ERROR", "Token is required");
|
||||
}
|
||||
return {
|
||||
access_token: mockTokens.access_token,
|
||||
refresh_token: mockTokens.refresh_token,
|
||||
user: mockCurrentUser,
|
||||
};
|
||||
},
|
||||
|
||||
async authRefresh(data: AuthRefreshRequest): Promise<AuthRefreshResponse> {
|
||||
await delay();
|
||||
if (!data.refresh_token) {
|
||||
throw mockError("VALIDATION_ERROR", "Refresh token is required");
|
||||
}
|
||||
return {
|
||||
access_token: `${mockTokens.access_token}_refreshed`,
|
||||
};
|
||||
},
|
||||
|
||||
async authMe() {
|
||||
await delay();
|
||||
return mockCurrentUser;
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Target Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getTargetChannels(params?: {
|
||||
is_active?: boolean;
|
||||
}): Promise<ListResponse<TargetChannel>> {
|
||||
await delay();
|
||||
let filtered = [...mockTargetChannels];
|
||||
|
||||
if (params?.is_active !== undefined) {
|
||||
filtered = filtered.filter((c) => c.is_active === params.is_active);
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
};
|
||||
},
|
||||
|
||||
async getTargetChannel(id: string) {
|
||||
await delay();
|
||||
const detail = getMockTargetChannelDetail(id);
|
||||
if (!detail) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
return detail;
|
||||
},
|
||||
|
||||
async updateTargetChannel(
|
||||
id: string,
|
||||
data: TargetChannelUpdateRequest
|
||||
): Promise<TargetChannel> {
|
||||
await delay();
|
||||
const channel = mockTargetChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
return {
|
||||
...channel,
|
||||
is_active: data.is_active,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async deleteTargetChannel(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const channel = mockTargetChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// External Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getExternalChannels(params?: {
|
||||
target_channel_id?: string;
|
||||
search?: string;
|
||||
}): Promise<ListResponse<ExternalChannel>> {
|
||||
await delay();
|
||||
let filtered = [...mockExternalChannels];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter((c) =>
|
||||
c.target_channels.includes(params.target_channel_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.search) {
|
||||
const search = params.search.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(c) =>
|
||||
c.title.toLowerCase().includes(search) ||
|
||||
c.username?.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
};
|
||||
},
|
||||
|
||||
async getExternalChannel(id: string) {
|
||||
await delay();
|
||||
const detail = getMockExternalChannelDetail(id);
|
||||
if (!detail) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
return detail;
|
||||
},
|
||||
|
||||
async createExternalChannel(
|
||||
data: ExternalChannelCreateRequest
|
||||
): Promise<ExternalChannel> {
|
||||
await delay(500);
|
||||
const newChannel: ExternalChannel = {
|
||||
id: generateId(),
|
||||
telegram_id: null,
|
||||
title: data.title,
|
||||
username: data.username || null,
|
||||
link: data.link,
|
||||
subscribers_count: data.subscribers_count || null,
|
||||
description: data.description || null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target_channels: data.target_channel_ids,
|
||||
total_purchases: 0,
|
||||
avg_cpf: null,
|
||||
avg_cpm: null,
|
||||
};
|
||||
mockExternalChannels.push(newChannel);
|
||||
return newChannel;
|
||||
},
|
||||
|
||||
async updateExternalChannel(
|
||||
id: string,
|
||||
data: ExternalChannelUpdateRequest
|
||||
): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
return {
|
||||
...channel,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async deleteExternalChannel(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const index = mockExternalChannels.findIndex((c) => c.id === id);
|
||||
if (index === -1) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
mockExternalChannels.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
async importExternalChannels(
|
||||
file: File,
|
||||
targetChannelId: string
|
||||
): Promise<ExternalChannelImportResponse> {
|
||||
await delay(1500); // Longer delay for file processing
|
||||
|
||||
// Mock: simulate importing 3-5 channels
|
||||
const importCount = Math.floor(Math.random() * 3) + 3;
|
||||
const imported: ExternalChannel[] = [];
|
||||
|
||||
for (let i = 0; i < importCount; i++) {
|
||||
const newChannel: ExternalChannel = {
|
||||
id: generateId(),
|
||||
telegram_id: null,
|
||||
title: `Импортированный канал ${i + 1}`,
|
||||
username: `imported_channel_${i + 1}`,
|
||||
link: `https://t.me/imported_channel_${i + 1}`,
|
||||
subscribers_count: Math.floor(Math.random() * 100000) + 10000,
|
||||
description: "Импортировано из Excel",
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target_channels: [targetChannelId],
|
||||
total_purchases: 0,
|
||||
avg_cpf: null,
|
||||
avg_cpm: null,
|
||||
};
|
||||
imported.push(newChannel);
|
||||
mockExternalChannels.push(newChannel);
|
||||
}
|
||||
|
||||
return {
|
||||
imported: importCount,
|
||||
skipped: 1,
|
||||
errors: ["Строка 15: некорректный формат ссылки"],
|
||||
channels: imported,
|
||||
};
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Creatives
|
||||
// --------------------------------------------------------------------------
|
||||
async getCreatives(params?: {
|
||||
target_channel_id?: string;
|
||||
is_archived?: boolean;
|
||||
}): Promise<ListResponse<Creative>> {
|
||||
await delay();
|
||||
let filtered = [...mockCreatives];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(c) => c.target_channel_id === params.target_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.is_archived !== undefined) {
|
||||
filtered = filtered.filter((c) => c.is_archived === params.is_archived);
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
};
|
||||
},
|
||||
|
||||
async getCreative(id: string) {
|
||||
await delay();
|
||||
const detail = getMockCreativeDetail(id);
|
||||
if (!detail) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
return detail;
|
||||
},
|
||||
|
||||
async createCreative(data: CreativeCreateRequest): Promise<Creative> {
|
||||
await delay(500);
|
||||
|
||||
// Validate {link} placeholder
|
||||
if (!data.text.includes("{link}")) {
|
||||
throw mockError(
|
||||
"VALIDATION_ERROR",
|
||||
"Creative text must contain {link} placeholder"
|
||||
);
|
||||
}
|
||||
|
||||
const targetChannel = mockTargetChannels.find(
|
||||
(c) => c.id === data.target_channel_id
|
||||
);
|
||||
if (!targetChannel) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
|
||||
const newCreative: Creative = {
|
||||
id: generateId(),
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
target_channel_id: data.target_channel_id,
|
||||
is_archived: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target_channel: targetChannel,
|
||||
total_purchases: 0,
|
||||
total_subscriptions: 0,
|
||||
avg_cpf: null,
|
||||
};
|
||||
mockCreatives.push(newCreative);
|
||||
return newCreative;
|
||||
},
|
||||
|
||||
async updateCreative(
|
||||
id: string,
|
||||
data: CreativeUpdateRequest
|
||||
): Promise<Creative> {
|
||||
await delay();
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
|
||||
if (data.text && !data.text.includes("{link}")) {
|
||||
throw mockError(
|
||||
"VALIDATION_ERROR",
|
||||
"Creative text must contain {link} placeholder"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...creative,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async deleteCreative(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
|
||||
if (creative.total_purchases > 0) {
|
||||
throw mockError(
|
||||
"CREATIVE_IN_USE",
|
||||
"Cannot delete creative with active purchases"
|
||||
);
|
||||
}
|
||||
|
||||
const index = mockCreatives.findIndex((c) => c.id === id);
|
||||
mockCreatives.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Purchases
|
||||
// --------------------------------------------------------------------------
|
||||
async getPurchases(params?: {
|
||||
target_channel_id?: string;
|
||||
external_channel_id?: string;
|
||||
creative_id?: string;
|
||||
is_archived?: boolean;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
sort?: string;
|
||||
order?: "asc" | "desc";
|
||||
}): Promise<ListResponse<Purchase>> {
|
||||
await delay();
|
||||
let filtered = [...mockPurchases];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(p) => p.target_channel_id === params.target_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.external_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(p) => p.external_channel_id === params.external_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.creative_id) {
|
||||
filtered = filtered.filter((p) => p.creative_id === params.creative_id);
|
||||
}
|
||||
|
||||
if (params?.is_archived !== undefined) {
|
||||
filtered = filtered.filter((p) => p.is_archived === params.is_archived);
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if (params?.sort) {
|
||||
filtered.sort((a, b) => {
|
||||
let aVal: any;
|
||||
let bVal: any;
|
||||
|
||||
switch (params.sort) {
|
||||
case "date":
|
||||
aVal = a.actual_date || a.scheduled_date || a.created_at;
|
||||
bVal = b.actual_date || b.scheduled_date || b.created_at;
|
||||
break;
|
||||
case "cost":
|
||||
aVal = a.cost || 0;
|
||||
bVal = b.cost || 0;
|
||||
break;
|
||||
case "cpf":
|
||||
aVal = a.cpf || 0;
|
||||
bVal = b.cpf || 0;
|
||||
break;
|
||||
case "subscriptions":
|
||||
aVal = a.subscriptions_count;
|
||||
bVal = b.subscriptions_count;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (params.order === "desc") {
|
||||
return bVal > aVal ? 1 : -1;
|
||||
}
|
||||
return aVal > bVal ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
};
|
||||
},
|
||||
|
||||
async getPurchase(id: string) {
|
||||
await delay();
|
||||
const detail = getMockPurchaseDetail(id);
|
||||
if (!detail) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
}
|
||||
return detail;
|
||||
},
|
||||
|
||||
async createPurchase(
|
||||
data: PurchaseCreateRequest
|
||||
): Promise<PurchaseCreateResponse> {
|
||||
await delay(800);
|
||||
|
||||
const targetChannel = mockTargetChannels.find(
|
||||
(c) => c.id === data.target_channel_id
|
||||
);
|
||||
const externalChannel = mockExternalChannels.find(
|
||||
(c) => c.id === data.external_channel_id
|
||||
);
|
||||
const creative = mockCreatives.find((c) => c.id === data.creative_id);
|
||||
|
||||
if (!targetChannel || !externalChannel || !creative) {
|
||||
throw mockError("NOT_FOUND", "One of the required entities not found");
|
||||
}
|
||||
|
||||
// Generate mock invite link
|
||||
const inviteLink = `https://t.me/+${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}`;
|
||||
|
||||
const newPurchase: Purchase = {
|
||||
id: generateId(),
|
||||
target_channel_id: data.target_channel_id,
|
||||
external_channel_id: data.external_channel_id,
|
||||
creative_id: data.creative_id,
|
||||
scheduled_date: data.scheduled_date || null,
|
||||
actual_date: data.actual_date || null,
|
||||
cost: data.cost || null,
|
||||
comment: data.comment || null,
|
||||
post_link: data.post_link || null,
|
||||
invite_link: inviteLink,
|
||||
invite_link_type: data.invite_link_type,
|
||||
is_archived: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target_channel: targetChannel,
|
||||
external_channel: externalChannel,
|
||||
creative: creative,
|
||||
subscriptions_count: 0,
|
||||
views_count: null,
|
||||
cpf: null,
|
||||
cpm: null,
|
||||
conversion_rate: null,
|
||||
};
|
||||
|
||||
mockPurchases.push(newPurchase);
|
||||
|
||||
// Format message with invite link
|
||||
const formattedMessage = creative.text.replace("{link}", inviteLink);
|
||||
|
||||
return {
|
||||
...newPurchase,
|
||||
formatted_message: formattedMessage,
|
||||
};
|
||||
},
|
||||
|
||||
async updatePurchase(
|
||||
id: string,
|
||||
data: PurchaseUpdateRequest
|
||||
): Promise<Purchase> {
|
||||
await delay();
|
||||
const purchase = mockPurchases.find((p) => p.id === id);
|
||||
if (!purchase) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...purchase,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async deletePurchase(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const index = mockPurchases.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
}
|
||||
mockPurchases.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
async refreshPurchaseViews(
|
||||
id: string
|
||||
): Promise<PurchaseRefreshViewsResponse> {
|
||||
await delay(1000);
|
||||
const purchase = mockPurchases.find((p) => p.id === id);
|
||||
if (!purchase) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
}
|
||||
|
||||
// Mock: add some random views
|
||||
const newViews =
|
||||
(purchase.views_count || 0) + Math.floor(Math.random() * 500);
|
||||
|
||||
return {
|
||||
views: newViews,
|
||||
fetched_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Analytics
|
||||
// --------------------------------------------------------------------------
|
||||
async getAnalyticsOverview(params?: {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}) {
|
||||
await delay();
|
||||
return mockAnalyticsOverview;
|
||||
},
|
||||
|
||||
async getAnalyticsCosts(params?: {
|
||||
period?: "day" | "week" | "month";
|
||||
target_channel_id?: string;
|
||||
}) {
|
||||
await delay();
|
||||
const period = params?.period || "week";
|
||||
|
||||
if (period === "day") {
|
||||
return mockCostsReportByDay;
|
||||
} else if (period === "month") {
|
||||
return mockCostsReportByMonth;
|
||||
}
|
||||
return mockCostsReportByWeek;
|
||||
},
|
||||
|
||||
async getAnalyticsChannels(params?: {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
type?: "external" | "target";
|
||||
}) {
|
||||
await delay();
|
||||
return {
|
||||
data: mockAnalyticsChannelsData,
|
||||
};
|
||||
},
|
||||
|
||||
async getAnalyticsCreatives(params?: {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}) {
|
||||
await delay();
|
||||
return {
|
||||
data: mockAnalyticsCreativesData,
|
||||
};
|
||||
},
|
||||
};
|
||||
10
lib/mocks/index.ts
Normal file
10
lib/mocks/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// ============================================================================
|
||||
// Mock Data Export
|
||||
// ============================================================================
|
||||
|
||||
export * from "./data/users";
|
||||
export * from "./data/channels";
|
||||
export * from "./data/external-channels";
|
||||
export * from "./data/creatives";
|
||||
export * from "./data/purchases";
|
||||
export * from "./data/analytics";
|
||||
Reference in New Issue
Block a user