improvement for the backend api
This commit is contained in:
@@ -6,13 +6,11 @@ import {
|
||||
mockCurrentUser,
|
||||
mockTokens,
|
||||
mockTargetChannels,
|
||||
getMockTargetChannelDetail,
|
||||
mockExternalChannels,
|
||||
getMockExternalChannelDetail,
|
||||
mockCreatives,
|
||||
getMockCreativeDetail,
|
||||
mockPurchases,
|
||||
getMockPurchaseDetail,
|
||||
mockPlacements,
|
||||
mockSubscriptions,
|
||||
mockViewsHistory,
|
||||
mockAnalyticsOverview,
|
||||
mockCostsReportByDay,
|
||||
mockCostsReportByWeek,
|
||||
@@ -28,22 +26,21 @@ import type {
|
||||
TargetChannel,
|
||||
ExternalChannel,
|
||||
Creative,
|
||||
Purchase,
|
||||
PurchaseCreateRequest,
|
||||
PurchaseCreateResponse,
|
||||
Placement,
|
||||
PlacementCreateRequest,
|
||||
PlacementUpdateRequest,
|
||||
CreativeCreateRequest,
|
||||
ExternalChannelCreateRequest,
|
||||
PurchaseRefreshViewsResponse,
|
||||
ExternalChannelImportResponse,
|
||||
ViewsFetchResponse,
|
||||
ViewsHistoryResponse,
|
||||
AuthCompleteRequest,
|
||||
AuthCompleteResponse,
|
||||
AuthRefreshRequest,
|
||||
AuthRefreshResponse,
|
||||
AuthInitResponse,
|
||||
TargetChannelUpdateRequest,
|
||||
ExternalChannelUpdateRequest,
|
||||
CreativeUpdateRequest,
|
||||
PurchaseUpdateRequest,
|
||||
ExternalChannelsListResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
// Simulate network delay
|
||||
@@ -75,15 +72,21 @@ export const mockApiHandlers = {
|
||||
};
|
||||
},
|
||||
|
||||
async authComplete(data: AuthCompleteRequest): Promise<AuthCompleteResponse> {
|
||||
async authComplete(
|
||||
data: AuthCompleteRequest | { token: string | null }
|
||||
): Promise<{ access_token: string }> {
|
||||
await delay();
|
||||
if (!data.token) {
|
||||
throw mockError("VALIDATION_ERROR", "Token is required");
|
||||
}
|
||||
|
||||
// Save mock user to localStorage for later retrieval
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("tgex_user", JSON.stringify(mockCurrentUser));
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: mockTokens.access_token,
|
||||
refresh_token: mockTokens.refresh_token,
|
||||
user: mockCurrentUser,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -105,44 +108,10 @@ export const mockApiHandlers = {
|
||||
// --------------------------------------------------------------------------
|
||||
// Target Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getTargetChannels(params?: {
|
||||
is_active?: boolean;
|
||||
}): Promise<ListResponse<TargetChannel>> {
|
||||
async getTargetChannels() {
|
||||
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(),
|
||||
target_channels: mockTargetChannels,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -158,41 +127,24 @@ export const mockApiHandlers = {
|
||||
// --------------------------------------------------------------------------
|
||||
// External Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getExternalChannels(params?: {
|
||||
target_channel_id?: string;
|
||||
search?: string;
|
||||
}): Promise<ListResponse<ExternalChannel>> {
|
||||
async getExternalChannels(
|
||||
targetChannelId: string
|
||||
): Promise<ExternalChannelsListResponse> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
// Возвращаем все каналы для указанного target channel
|
||||
// В реальном API это будет фильтрация по связям через промежуточную таблицу
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
external_channels: mockExternalChannels,
|
||||
};
|
||||
},
|
||||
|
||||
async getExternalChannel(id: string) {
|
||||
async getExternalChannel(id: string): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const detail = getMockExternalChannelDetail(id);
|
||||
if (!detail) {
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
return detail;
|
||||
return channel;
|
||||
},
|
||||
|
||||
async createExternalChannel(
|
||||
@@ -201,18 +153,11 @@ export const mockApiHandlers = {
|
||||
await delay(500);
|
||||
const newChannel: ExternalChannel = {
|
||||
id: generateId(),
|
||||
telegram_id: null,
|
||||
telegram_id: data.telegram_id,
|
||||
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;
|
||||
@@ -230,7 +175,6 @@ export const mockApiHandlers = {
|
||||
return {
|
||||
...channel,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -244,42 +188,20 @@ export const mockApiHandlers = {
|
||||
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);
|
||||
async updateExternalChannelLinks(
|
||||
id: string,
|
||||
data: {
|
||||
add_target_channel_ids?: string[];
|
||||
remove_target_channel_ids?: string[];
|
||||
}
|
||||
|
||||
return {
|
||||
imported: importCount,
|
||||
skipped: 1,
|
||||
errors: ["Строка 15: некорректный формат ссылки"],
|
||||
channels: imported,
|
||||
};
|
||||
): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
// В упрощенной версии просто возвращаем канал
|
||||
return channel;
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -287,8 +209,8 @@ export const mockApiHandlers = {
|
||||
// --------------------------------------------------------------------------
|
||||
async getCreatives(params?: {
|
||||
target_channel_id?: string;
|
||||
is_archived?: boolean;
|
||||
}): Promise<ListResponse<Creative>> {
|
||||
include_archived?: boolean;
|
||||
}) {
|
||||
await delay();
|
||||
let filtered = [...mockCreatives];
|
||||
|
||||
@@ -298,36 +220,27 @@ export const mockApiHandlers = {
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.is_archived !== undefined) {
|
||||
filtered = filtered.filter((c) => c.is_archived === params.is_archived);
|
||||
if (!params?.include_archived) {
|
||||
filtered = filtered.filter((c) => c.status !== "archived");
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
creatives: filtered,
|
||||
};
|
||||
},
|
||||
|
||||
async getCreative(id: string) {
|
||||
async getCreative(id: string): Promise<Creative> {
|
||||
await delay();
|
||||
const detail = getMockCreativeDetail(id);
|
||||
if (!detail) {
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
return detail;
|
||||
return creative;
|
||||
},
|
||||
|
||||
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
|
||||
);
|
||||
@@ -340,13 +253,10 @@ export const mockApiHandlers = {
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
target_channel_id: data.target_channel_id,
|
||||
is_archived: false,
|
||||
target_channel_title: targetChannel.title,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
target_channel: targetChannel,
|
||||
total_purchases: 0,
|
||||
total_subscriptions: 0,
|
||||
avg_cpf: null,
|
||||
status: "active",
|
||||
placements_count: 0,
|
||||
};
|
||||
mockCreatives.push(newCreative);
|
||||
return newCreative;
|
||||
@@ -372,7 +282,6 @@ export const mockApiHandlers = {
|
||||
return {
|
||||
...creative,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -383,10 +292,10 @@ export const mockApiHandlers = {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
|
||||
if (creative.total_purchases > 0) {
|
||||
if (creative.placements_count > 0) {
|
||||
throw mockError(
|
||||
"CREATIVE_IN_USE",
|
||||
"Cannot delete creative with active purchases"
|
||||
"Cannot delete creative with active placements"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -396,20 +305,16 @@ export const mockApiHandlers = {
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Purchases
|
||||
// Placements (Purchases)
|
||||
// --------------------------------------------------------------------------
|
||||
async getPurchases(params?: {
|
||||
async getPlacements(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>> {
|
||||
include_archived?: boolean;
|
||||
}) {
|
||||
await delay();
|
||||
let filtered = [...mockPurchases];
|
||||
let filtered = [...mockPlacements];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
@@ -427,62 +332,25 @@ export const mockApiHandlers = {
|
||||
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;
|
||||
});
|
||||
if (!params?.include_archived) {
|
||||
filtered = filtered.filter((p) => p.status !== "archived");
|
||||
}
|
||||
|
||||
return {
|
||||
data: filtered,
|
||||
total: filtered.length,
|
||||
placements: filtered,
|
||||
};
|
||||
},
|
||||
|
||||
async getPurchase(id: string) {
|
||||
async getPlacement(id: string): Promise<Placement> {
|
||||
await delay();
|
||||
const detail = getMockPurchaseDetail(id);
|
||||
if (!detail) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
return detail;
|
||||
return placement;
|
||||
},
|
||||
|
||||
async createPurchase(
|
||||
data: PurchaseCreateRequest
|
||||
): Promise<PurchaseCreateResponse> {
|
||||
async createPlacement(data: PlacementCreateRequest): Promise<Placement> {
|
||||
await delay(800);
|
||||
|
||||
const targetChannel = mockTargetChannels.find(
|
||||
@@ -502,85 +370,111 @@ export const mockApiHandlers = {
|
||||
.toString(36)
|
||||
.substring(2, 15)}`;
|
||||
|
||||
const newPurchase: Purchase = {
|
||||
const newPlacement: Placement = {
|
||||
id: generateId(),
|
||||
target_channel_id: data.target_channel_id,
|
||||
target_channel_title: targetChannel.title,
|
||||
external_channel_id: data.external_channel_id,
|
||||
external_channel_title: externalChannel.title,
|
||||
creative_id: data.creative_id,
|
||||
scheduled_date: data.scheduled_date || null,
|
||||
actual_date: data.actual_date || null,
|
||||
creative_name: creative.name,
|
||||
placement_date: data.placement_date,
|
||||
cost: data.cost || null,
|
||||
comment: data.comment || null,
|
||||
post_link: data.post_link || null,
|
||||
ad_post_url: data.ad_post_url || null,
|
||||
invite_link_type: data.invite_link_type || "public",
|
||||
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,
|
||||
status: "active",
|
||||
subscriptions_count: 0,
|
||||
views_count: null,
|
||||
cpf: null,
|
||||
cpm: null,
|
||||
conversion_rate: null,
|
||||
views_availability: "unknown",
|
||||
last_views_fetch_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockPurchases.push(newPurchase);
|
||||
|
||||
// Format message with invite link
|
||||
const formattedMessage = creative.text.replace("{link}", inviteLink);
|
||||
|
||||
return {
|
||||
...newPurchase,
|
||||
formatted_message: formattedMessage,
|
||||
};
|
||||
mockPlacements.push(newPlacement);
|
||||
return newPlacement;
|
||||
},
|
||||
|
||||
async updatePurchase(
|
||||
async updatePlacement(
|
||||
id: string,
|
||||
data: PurchaseUpdateRequest
|
||||
): Promise<Purchase> {
|
||||
data: PlacementUpdateRequest
|
||||
): Promise<Placement> {
|
||||
await delay();
|
||||
const purchase = mockPurchases.find((p) => p.id === id);
|
||||
if (!purchase) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...purchase,
|
||||
...placement,
|
||||
...data,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
async deletePurchase(id: string): Promise<SuccessResponse> {
|
||||
async deletePlacement(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const index = mockPurchases.findIndex((p) => p.id === id);
|
||||
const index = mockPlacements.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
mockPurchases.splice(index, 1);
|
||||
mockPlacements.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
async refreshPurchaseViews(
|
||||
id: string
|
||||
): Promise<PurchaseRefreshViewsResponse> {
|
||||
async fetchPlacementViews(id: string): Promise<ViewsFetchResponse> {
|
||||
await delay(1000);
|
||||
const purchase = mockPurchases.find((p) => p.id === id);
|
||||
if (!purchase) {
|
||||
throw mockError("NOT_FOUND", "Purchase not found");
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
// Mock: add some random views
|
||||
const newViews =
|
||||
(purchase.views_count || 0) + Math.floor(Math.random() * 500);
|
||||
(placement.views_count || 0) + Math.floor(Math.random() * 500);
|
||||
|
||||
return {
|
||||
views: newViews,
|
||||
placement_id: id,
|
||||
views_count: newViews,
|
||||
views_availability: "available",
|
||||
fetched_at: new Date().toISOString(),
|
||||
error_message: null,
|
||||
};
|
||||
},
|
||||
|
||||
async getPlacementViewsHistory(
|
||||
id: string,
|
||||
params?: { from_date?: string; to_date?: string }
|
||||
): Promise<ViewsHistoryResponse> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
const filteredHistory = mockViewsHistory.filter(
|
||||
(v) => v.purchase_id === id
|
||||
);
|
||||
return {
|
||||
histories: filteredHistory,
|
||||
};
|
||||
},
|
||||
|
||||
async setPlacementViewsManually(
|
||||
id: string,
|
||||
views_count: number
|
||||
): Promise<Placement> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...placement,
|
||||
views_count,
|
||||
views_availability: "manual",
|
||||
last_views_fetch_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user