mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
Merge remote-tracking branch 'origin/dev_nikita' into dev_nikita
This commit is contained in:
@@ -49,6 +49,20 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Telegram OIDC authentication (popup flow with id_token)
|
||||
loginTelegramOIDC: async (
|
||||
idToken: string,
|
||||
campaignSlug?: string | null,
|
||||
referralCode?: string | null,
|
||||
): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/telegram/oidc', {
|
||||
id_token: idToken,
|
||||
campaign_slug: campaignSlug || undefined,
|
||||
referral_code: referralCode || undefined,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Email login
|
||||
loginEmail: async (
|
||||
email: string,
|
||||
@@ -234,10 +248,11 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Link Telegram account (Mini App initData or Login Widget data)
|
||||
// Link Telegram account (Mini App initData, OIDC id_token, or Login Widget data)
|
||||
linkTelegram: async (
|
||||
data:
|
||||
| { init_data: string }
|
||||
| { id_token: string }
|
||||
| {
|
||||
id: number;
|
||||
first_name: string;
|
||||
@@ -280,6 +295,12 @@ export const authApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Auto-login from guest purchase success page
|
||||
autoLogin: async (token: string): Promise<AuthResponse> => {
|
||||
const response = await apiClient.post<AuthResponse>('/cabinet/auth/login/auto', { token });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Account merge (no JWT required)
|
||||
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
|
||||
const response = await apiClient.get<MergePreviewResponse>(
|
||||
|
||||
@@ -110,7 +110,15 @@ export const balanceApi = {
|
||||
// Get specific pending payment details
|
||||
getPendingPayment: async (method: string, paymentId: number): Promise<PendingPayment> => {
|
||||
const response = await apiClient.get<PendingPayment>(
|
||||
`/cabinet/balance/pending-payments/${method}/${paymentId}`,
|
||||
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/${encodeURIComponent(paymentId)}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get latest pending payment by method (fallback when sessionStorage unavailable)
|
||||
getLatestPayment: async (method: string): Promise<PendingPayment> => {
|
||||
const response = await apiClient.get<PendingPayment>(
|
||||
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/latest`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
@@ -118,7 +126,7 @@ export const balanceApi = {
|
||||
// Manually check payment status
|
||||
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
|
||||
const response = await apiClient.post<ManualCheckResponse>(
|
||||
`/cabinet/balance/pending-payments/${method}/${paymentId}/check`,
|
||||
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/${encodeURIComponent(paymentId)}/check`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -23,6 +23,20 @@ export interface EmailAuthEnabled {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface GiftEnabled {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface TelegramWidgetConfig {
|
||||
bot_username: string;
|
||||
size: 'large' | 'medium' | 'small';
|
||||
radius: number;
|
||||
userpic: boolean;
|
||||
request_access: boolean;
|
||||
oidc_enabled: boolean;
|
||||
oidc_client_id: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsCounters {
|
||||
yandex_metrika_id: string;
|
||||
google_ads_id: string;
|
||||
@@ -241,6 +255,24 @@ export const brandingApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get gift enabled (public, no auth required)
|
||||
getGiftEnabled: async (): Promise<GiftEnabled> => {
|
||||
try {
|
||||
const response = await apiClient.get<GiftEnabled>('/cabinet/branding/gift-enabled');
|
||||
return response.data;
|
||||
} catch {
|
||||
return { enabled: false };
|
||||
}
|
||||
},
|
||||
|
||||
// Update gift enabled (admin only)
|
||||
updateGiftEnabled: async (enabled: boolean): Promise<GiftEnabled> => {
|
||||
const response = await apiClient.patch<GiftEnabled>('/cabinet/branding/gift-enabled', {
|
||||
enabled,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get analytics counters (public, no auth required)
|
||||
getAnalyticsCounters: async (): Promise<AnalyticsCounters> => {
|
||||
try {
|
||||
@@ -251,6 +283,26 @@ export const brandingApi = {
|
||||
}
|
||||
},
|
||||
|
||||
// Get Telegram widget config (public, no auth required)
|
||||
getTelegramWidgetConfig: async (): Promise<TelegramWidgetConfig> => {
|
||||
try {
|
||||
const response = await apiClient.get<TelegramWidgetConfig>(
|
||||
'/cabinet/branding/telegram-widget',
|
||||
);
|
||||
return response.data;
|
||||
} catch {
|
||||
return {
|
||||
bot_username: import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '',
|
||||
size: 'large',
|
||||
radius: 8,
|
||||
userpic: true,
|
||||
request_access: true,
|
||||
oidc_enabled: false,
|
||||
oidc_client_id: '',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Update analytics counters (admin only)
|
||||
updateAnalyticsCounters: async (data: Partial<AnalyticsCounters>): Promise<AnalyticsCounters> => {
|
||||
const response = await apiClient.patch<AnalyticsCounters>('/cabinet/branding/analytics', data);
|
||||
|
||||
@@ -77,6 +77,7 @@ const AUTH_ENDPOINTS = [
|
||||
'/cabinet/auth/oauth/',
|
||||
'/cabinet/auth/merge/',
|
||||
'/cabinet/auth/account/link/server-complete',
|
||||
'/cabinet/landing/',
|
||||
];
|
||||
|
||||
function isAuthEndpoint(url: string | undefined): boolean {
|
||||
@@ -91,18 +92,22 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
if (!isAuthEndpoint(config.url)) {
|
||||
let token = tokenStorage.getAccessToken();
|
||||
|
||||
// Проверяем срок действия токена перед запросом
|
||||
if (token && isTokenExpired(token)) {
|
||||
// Используем централизованный менеджер для refresh
|
||||
// Access token expired — try refresh
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
} else {
|
||||
// Refresh не удался - редирект на логин
|
||||
tokenStorage.clearTokens();
|
||||
safeRedirectToLogin();
|
||||
return config;
|
||||
}
|
||||
} else if (!token && tokenStorage.getRefreshToken()) {
|
||||
// No access token (e.g. tab reopen) but refresh token exists — restore session
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (token && config.headers) {
|
||||
|
||||
157
src/api/gift.ts
Normal file
157
src/api/gift.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import apiClient from './client';
|
||||
|
||||
// Types
|
||||
|
||||
export interface GiftTariffPeriod {
|
||||
days: number;
|
||||
price_kopeks: number;
|
||||
price_label: string;
|
||||
original_price_kopeks: number | null;
|
||||
discount_percent: number | null;
|
||||
}
|
||||
|
||||
export interface GiftTariff {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
periods: GiftTariffPeriod[];
|
||||
}
|
||||
|
||||
export interface GiftPaymentMethodSubOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface GiftPaymentMethod {
|
||||
method_id: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
icon_url: string | null;
|
||||
min_amount_kopeks: number | null;
|
||||
max_amount_kopeks: number | null;
|
||||
sub_options: GiftPaymentMethodSubOption[] | null;
|
||||
}
|
||||
|
||||
export interface GiftConfig {
|
||||
is_enabled: boolean;
|
||||
tariffs: GiftTariff[];
|
||||
payment_methods: GiftPaymentMethod[];
|
||||
balance_kopeks: number;
|
||||
currency_symbol: string;
|
||||
}
|
||||
|
||||
export interface GiftPurchaseRequest {
|
||||
tariff_id: number;
|
||||
period_days: number;
|
||||
recipient_type?: 'email' | 'telegram';
|
||||
recipient_value?: string;
|
||||
gift_message?: string;
|
||||
payment_mode: 'balance' | 'gateway';
|
||||
payment_method?: string;
|
||||
}
|
||||
|
||||
export interface GiftPurchaseResponse {
|
||||
status: 'ok' | 'created' | 'paid';
|
||||
purchase_token: string;
|
||||
payment_url: string | null;
|
||||
warning: string | null;
|
||||
}
|
||||
|
||||
export type GiftPurchaseStatusValue =
|
||||
| 'pending'
|
||||
| 'paid'
|
||||
| 'delivered'
|
||||
| 'pending_activation'
|
||||
| 'failed'
|
||||
| 'expired';
|
||||
|
||||
export interface GiftPurchaseStatus {
|
||||
status: GiftPurchaseStatusValue;
|
||||
is_gift: boolean;
|
||||
is_code_only: boolean;
|
||||
purchase_token: string | null;
|
||||
recipient_contact_value: string | null;
|
||||
gift_message: string | null;
|
||||
tariff_name: string | null;
|
||||
period_days: number | null;
|
||||
warning: string | null;
|
||||
}
|
||||
|
||||
export interface PendingGift {
|
||||
token: string;
|
||||
tariff_name: string | null;
|
||||
period_days: number;
|
||||
gift_message: string | null;
|
||||
sender_display: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface SentGift {
|
||||
token: string;
|
||||
tariff_name: string | null;
|
||||
period_days: number;
|
||||
device_limit: number;
|
||||
status: string;
|
||||
gift_recipient_value: string | null;
|
||||
gift_message: string | null;
|
||||
activated_by_username: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface ReceivedGift {
|
||||
token: string;
|
||||
tariff_name: string | null;
|
||||
period_days: number;
|
||||
device_limit: number;
|
||||
status: string;
|
||||
sender_display: string | null;
|
||||
gift_message: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
export interface ActivateGiftResponse {
|
||||
status: string;
|
||||
tariff_name: string | null;
|
||||
period_days: number | null;
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
export const giftApi = {
|
||||
getConfig: async (): Promise<GiftConfig> => {
|
||||
const { data } = await apiClient.get<GiftConfig>('/cabinet/gift/config');
|
||||
return data;
|
||||
},
|
||||
|
||||
createPurchase: async (request: GiftPurchaseRequest): Promise<GiftPurchaseResponse> => {
|
||||
const { data } = await apiClient.post<GiftPurchaseResponse>('/cabinet/gift/purchase', request);
|
||||
return data;
|
||||
},
|
||||
|
||||
getPurchaseStatus: async (token: string): Promise<GiftPurchaseStatus> => {
|
||||
const { data } = await apiClient.get<GiftPurchaseStatus>(`/cabinet/gift/purchase/${token}`);
|
||||
return data;
|
||||
},
|
||||
|
||||
getPendingGifts: async (): Promise<PendingGift[]> => {
|
||||
const { data } = await apiClient.get<PendingGift[]>('/cabinet/gift/pending');
|
||||
return data;
|
||||
},
|
||||
|
||||
getSentGifts: async (): Promise<SentGift[]> => {
|
||||
const { data } = await apiClient.get<SentGift[]>('/cabinet/gift/sent');
|
||||
return data;
|
||||
},
|
||||
|
||||
getReceivedGifts: async (): Promise<ReceivedGift[]> => {
|
||||
const { data } = await apiClient.get<ReceivedGift[]>('/cabinet/gift/received');
|
||||
return data;
|
||||
},
|
||||
|
||||
activateGiftCode: async (code: string): Promise<ActivateGiftResponse> => {
|
||||
const { data } = await apiClient.post<ActivateGiftResponse>('/cabinet/gift/activate', { code });
|
||||
return data;
|
||||
},
|
||||
};
|
||||
404
src/api/landings.ts
Normal file
404
src/api/landings.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import apiClient from './client';
|
||||
import type { AnimationConfig } from '@/components/ui/backgrounds/types';
|
||||
|
||||
// ============================================================
|
||||
// Public types
|
||||
// ============================================================
|
||||
|
||||
export interface LandingFeature {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface LandingTariffPeriod {
|
||||
days: number;
|
||||
label: string;
|
||||
price_kopeks: number;
|
||||
price_label: string;
|
||||
original_price_kopeks: number | null;
|
||||
original_price_label: string | null;
|
||||
discount_percent: number | null;
|
||||
}
|
||||
|
||||
export interface LandingTariff {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string | null;
|
||||
traffic_limit_gb: number;
|
||||
device_limit: number;
|
||||
tier_level: number;
|
||||
periods: LandingTariffPeriod[];
|
||||
}
|
||||
|
||||
export interface LandingPaymentMethodSubOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Payment method as returned by the public landing config API */
|
||||
export interface LandingPaymentMethod {
|
||||
method_id: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
icon_url: string | null;
|
||||
sort_order: number;
|
||||
min_amount_kopeks: number | null;
|
||||
max_amount_kopeks: number | null;
|
||||
currency: string | null;
|
||||
sub_options: LandingPaymentMethodSubOption[] | null;
|
||||
}
|
||||
|
||||
/** Payment method as stored/returned by the admin landing API (sub_options is a dict) */
|
||||
export interface AdminLandingPaymentMethod {
|
||||
method_id: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
icon_url: string | null;
|
||||
sort_order: number;
|
||||
min_amount_kopeks: number | null;
|
||||
max_amount_kopeks: number | null;
|
||||
currency: string | null;
|
||||
return_url: string | null;
|
||||
sub_options: Record<string, boolean> | null;
|
||||
}
|
||||
|
||||
/** Editable fields on a payment method in the landing editor */
|
||||
export type EditableMethodField =
|
||||
| 'display_name'
|
||||
| 'description'
|
||||
| 'icon_url'
|
||||
| 'min_amount_kopeks'
|
||||
| 'max_amount_kopeks'
|
||||
| 'currency'
|
||||
| 'return_url';
|
||||
|
||||
export interface LandingDiscountInfo {
|
||||
percent: number;
|
||||
ends_at: string; // ISO datetime
|
||||
badge_text: string | null;
|
||||
}
|
||||
|
||||
export interface LandingConfig {
|
||||
slug: string;
|
||||
title: string;
|
||||
subtitle: string | null;
|
||||
features: LandingFeature[];
|
||||
footer_text: string | null;
|
||||
tariffs: LandingTariff[];
|
||||
payment_methods: LandingPaymentMethod[];
|
||||
gift_enabled: boolean;
|
||||
custom_css: string | null;
|
||||
meta_title: string | null;
|
||||
meta_description: string | null;
|
||||
discount: LandingDiscountInfo | null;
|
||||
background_config: AnimationConfig | null;
|
||||
}
|
||||
|
||||
export interface PurchaseRequest {
|
||||
tariff_id: number;
|
||||
period_days: number;
|
||||
contact_type: 'email' | 'telegram';
|
||||
contact_value: string;
|
||||
payment_method: string;
|
||||
is_gift: boolean;
|
||||
gift_recipient_type?: 'email' | 'telegram';
|
||||
gift_recipient_value?: string;
|
||||
gift_message?: string;
|
||||
}
|
||||
|
||||
export interface PurchaseResponse {
|
||||
purchase_token: string;
|
||||
payment_url: string;
|
||||
}
|
||||
|
||||
export interface PurchaseStatus {
|
||||
status: 'pending' | 'paid' | 'delivered' | 'pending_activation' | 'failed' | 'expired';
|
||||
subscription_url: string | null;
|
||||
subscription_crypto_link: string | null;
|
||||
is_gift: boolean;
|
||||
contact_value: string | null;
|
||||
recipient_contact_value: string | null;
|
||||
period_days: number | null;
|
||||
tariff_name: string | null;
|
||||
gift_message: string | null;
|
||||
contact_type: 'email' | 'telegram' | null;
|
||||
cabinet_email: string | null;
|
||||
cabinet_password: string | null;
|
||||
auto_login_token: string | null;
|
||||
recipient_in_bot: boolean | null;
|
||||
bot_link: string | null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Locale helpers
|
||||
// ============================================================
|
||||
|
||||
/** Locale dict for multi-language text fields (admin API) */
|
||||
export type LocaleDict = Record<string, string>;
|
||||
|
||||
/** Supported locales for the admin editor */
|
||||
export const SUPPORTED_LOCALES = ['ru', 'en', 'zh', 'fa'] as const;
|
||||
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||
|
||||
export const LOCALE_META: Record<SupportedLocale, { flag: string; name: string; rtl: boolean }> = {
|
||||
ru: { flag: '\u{1F1F7}\u{1F1FA}', name: 'RU', rtl: false },
|
||||
en: { flag: '\u{1F1EC}\u{1F1E7}', name: 'EN', rtl: false },
|
||||
zh: { flag: '\u{1F1E8}\u{1F1F3}', name: 'ZH', rtl: false },
|
||||
fa: { flag: '\u{1F1EE}\u{1F1F7}', name: 'FA', rtl: true },
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Admin types
|
||||
// ============================================================
|
||||
|
||||
/** Admin feature type with localized title/description */
|
||||
export interface AdminLandingFeature {
|
||||
icon: string;
|
||||
title: LocaleDict;
|
||||
description: LocaleDict;
|
||||
}
|
||||
|
||||
export interface LandingListItem {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: LocaleDict;
|
||||
is_active: boolean;
|
||||
display_order: number;
|
||||
gift_enabled: boolean;
|
||||
tariff_count: number;
|
||||
method_count: number;
|
||||
purchase_stats: {
|
||||
total: number;
|
||||
pending: number;
|
||||
paid: number;
|
||||
delivered: number;
|
||||
pending_activation: number;
|
||||
failed: number;
|
||||
expired: number;
|
||||
};
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
has_active_discount: boolean;
|
||||
}
|
||||
|
||||
export interface LandingDetail {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: LocaleDict;
|
||||
subtitle: LocaleDict | null;
|
||||
is_active: boolean;
|
||||
features: AdminLandingFeature[];
|
||||
footer_text: LocaleDict | null;
|
||||
allowed_tariff_ids: number[];
|
||||
allowed_periods: Record<string, number[]>;
|
||||
payment_methods: AdminLandingPaymentMethod[];
|
||||
gift_enabled: boolean;
|
||||
custom_css: string | null;
|
||||
meta_title: LocaleDict | null;
|
||||
meta_description: LocaleDict | null;
|
||||
display_order: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
discount_percent: number | null;
|
||||
discount_overrides: Record<string, number> | null;
|
||||
discount_starts_at: string | null;
|
||||
discount_ends_at: string | null;
|
||||
discount_badge_text: LocaleDict | null;
|
||||
background_config: AnimationConfig | null;
|
||||
}
|
||||
|
||||
export interface LandingCreateRequest {
|
||||
slug: string;
|
||||
title: LocaleDict;
|
||||
subtitle?: LocaleDict;
|
||||
is_active?: boolean;
|
||||
features?: AdminLandingFeature[];
|
||||
footer_text?: LocaleDict;
|
||||
allowed_tariff_ids?: number[];
|
||||
allowed_periods?: Record<string, number[]>;
|
||||
payment_methods?: AdminLandingPaymentMethod[];
|
||||
gift_enabled?: boolean;
|
||||
custom_css?: string;
|
||||
meta_title?: LocaleDict;
|
||||
meta_description?: LocaleDict;
|
||||
discount_percent?: number | null;
|
||||
discount_overrides?: Record<string, number> | null;
|
||||
discount_starts_at?: string | null;
|
||||
discount_ends_at?: string | null;
|
||||
discount_badge_text?: LocaleDict | null;
|
||||
background_config?: AnimationConfig | null;
|
||||
}
|
||||
|
||||
export type LandingUpdateRequest = Partial<LandingCreateRequest>;
|
||||
|
||||
/**
|
||||
* Normalize a value that might be a plain string (old API) or a LocaleDict.
|
||||
* If it's a string, wraps it as `{ ru: value }`.
|
||||
* If null/undefined, returns the fallback.
|
||||
*/
|
||||
/** Extract best display string from a LocaleDict: ru → en → first available → '' */
|
||||
export function resolveLocaleDisplay(dict: LocaleDict | string | null | undefined): string {
|
||||
if (!dict) return '';
|
||||
if (typeof dict === 'string') return dict;
|
||||
return dict.ru || dict.en || Object.values(dict).find((v) => v?.trim()) || '';
|
||||
}
|
||||
|
||||
export function toLocaleDict(
|
||||
value: string | LocaleDict | null | undefined,
|
||||
fallback: LocaleDict = {},
|
||||
): LocaleDict {
|
||||
if (value === null || value === undefined) return fallback;
|
||||
if (typeof value === 'string') return value ? { ru: value } : fallback;
|
||||
return value;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Public API
|
||||
// ============================================================
|
||||
|
||||
export const landingApi = {
|
||||
getConfig: async (slug: string, lang?: string): Promise<LandingConfig> => {
|
||||
const params = lang ? `?lang=${lang}` : '';
|
||||
const response = await apiClient.get(`/cabinet/landing/${slug}${params}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createPurchase: async (slug: string, data: PurchaseRequest): Promise<PurchaseResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/landing/${slug}/purchase`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPurchaseStatus: async (token: string): Promise<PurchaseStatus> => {
|
||||
const response = await apiClient.get(`/cabinet/landing/purchase/${token}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
activatePurchase: async (token: string): Promise<PurchaseStatus> => {
|
||||
const response = await apiClient.post(`/cabinet/landing/activate/${token}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Admin stats types
|
||||
// ============================================================
|
||||
|
||||
export interface LandingDailyStat {
|
||||
date: string;
|
||||
purchases: number;
|
||||
revenue_kopeks: number;
|
||||
gifts: number;
|
||||
}
|
||||
|
||||
export interface LandingTariffStat {
|
||||
tariff_id: number | null;
|
||||
tariff_name: string;
|
||||
purchases: number;
|
||||
revenue_kopeks: number;
|
||||
}
|
||||
|
||||
export interface LandingStatsResponse {
|
||||
total_purchases: number;
|
||||
total_revenue_kopeks: number;
|
||||
total_gifts: number;
|
||||
total_regular: number;
|
||||
avg_purchase_kopeks: number;
|
||||
total_created: number;
|
||||
total_successful: number;
|
||||
conversion_rate: number;
|
||||
daily_stats: LandingDailyStat[];
|
||||
tariff_stats: LandingTariffStat[];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Admin purchase list types
|
||||
// ============================================================
|
||||
|
||||
export type PurchaseItemStatus =
|
||||
| 'pending'
|
||||
| 'paid'
|
||||
| 'delivered'
|
||||
| 'pending_activation'
|
||||
| 'failed'
|
||||
| 'expired';
|
||||
|
||||
export interface LandingPurchaseItem {
|
||||
id: number;
|
||||
token: string;
|
||||
contact_type: 'email' | 'telegram';
|
||||
contact_value: string;
|
||||
is_gift: boolean;
|
||||
gift_recipient_type: 'email' | 'telegram' | null;
|
||||
gift_recipient_value: string | null;
|
||||
tariff_name: string;
|
||||
period_days: number;
|
||||
amount_kopeks: number;
|
||||
currency: string;
|
||||
payment_method: string;
|
||||
status: PurchaseItemStatus;
|
||||
created_at: string;
|
||||
paid_at: string | null;
|
||||
}
|
||||
|
||||
export interface LandingPurchaseListResponse {
|
||||
items: LandingPurchaseItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Admin API
|
||||
// ============================================================
|
||||
|
||||
export const adminLandingsApi = {
|
||||
list: async (): Promise<LandingListItem[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/landings');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (id: number): Promise<LandingDetail> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/landings/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
create: async (data: LandingCreateRequest): Promise<LandingDetail> => {
|
||||
const response = await apiClient.post('/cabinet/admin/landings', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
update: async (id: number, data: LandingUpdateRequest): Promise<LandingDetail> => {
|
||||
const response = await apiClient.put(`/cabinet/admin/landings/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
delete: async (id: number): Promise<{ success: boolean }> => {
|
||||
const response = await apiClient.delete(`/cabinet/admin/landings/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
toggle: async (id: number): Promise<LandingDetail> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/landings/${id}/toggle`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
reorder: async (landingIds: number[]): Promise<void> => {
|
||||
await apiClient.put('/cabinet/admin/landings/order', { landing_ids: landingIds });
|
||||
},
|
||||
|
||||
getStats: async (id: number): Promise<LandingStatsResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/landings/${id}/stats`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPurchases: async (
|
||||
id: number,
|
||||
offset: number,
|
||||
limit: number,
|
||||
status?: PurchaseItemStatus,
|
||||
): Promise<LandingPurchaseListResponse> => {
|
||||
const params: Record<string, string | number> = { offset, limit };
|
||||
if (status) params.status = status;
|
||||
const response = await apiClient.get(`/cabinet/admin/landings/${id}/purchases`, { params });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
92
src/api/menuLayout.ts
Normal file
92
src/api/menuLayout.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export type OpenIn = 'external' | 'webapp';
|
||||
|
||||
export interface MenuButtonConfig {
|
||||
id: string;
|
||||
type: 'builtin' | 'custom';
|
||||
style: 'primary' | 'success' | 'danger' | 'default';
|
||||
icon_custom_emoji_id: string;
|
||||
enabled: boolean;
|
||||
labels: Record<string, string>;
|
||||
url: string | null;
|
||||
open_in: OpenIn;
|
||||
}
|
||||
|
||||
export interface MenuRowConfig {
|
||||
id: string;
|
||||
max_per_row: number;
|
||||
buttons: MenuButtonConfig[];
|
||||
}
|
||||
|
||||
export interface MenuConfig {
|
||||
rows: MenuRowConfig[];
|
||||
}
|
||||
|
||||
export const BOT_LOCALES = ['ru', 'en', 'ua', 'zh', 'fa'] as const;
|
||||
export type BotLocale = (typeof BOT_LOCALES)[number];
|
||||
|
||||
export const BUILTIN_SECTIONS = [
|
||||
'home',
|
||||
'subscription',
|
||||
'balance',
|
||||
'referral',
|
||||
'support',
|
||||
'info',
|
||||
'admin',
|
||||
'language',
|
||||
] as const;
|
||||
|
||||
export type BuiltinSection = (typeof BUILTIN_SECTIONS)[number];
|
||||
|
||||
export const STYLE_OPTIONS = [
|
||||
{ value: 'default' as const, colorClass: 'bg-dark-500' },
|
||||
{ value: 'primary' as const, colorClass: 'bg-blue-500' },
|
||||
{ value: 'success' as const, colorClass: 'bg-success-500' },
|
||||
{ value: 'danger' as const, colorClass: 'bg-red-500' },
|
||||
];
|
||||
|
||||
const DEFAULT_CONFIG: MenuConfig = { rows: [] };
|
||||
|
||||
const DEFAULT_BUTTON: Omit<MenuButtonConfig, 'id' | 'type'> = {
|
||||
style: 'primary',
|
||||
icon_custom_emoji_id: '',
|
||||
enabled: true,
|
||||
labels: {},
|
||||
url: null,
|
||||
open_in: 'external',
|
||||
};
|
||||
|
||||
function normalizeConfig(data: MenuConfig): MenuConfig {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
return {
|
||||
rows: (data.rows || []).map((row) => ({
|
||||
id: row.id,
|
||||
max_per_row: row.max_per_row ?? 2,
|
||||
buttons: (row.buttons || []).map((btn) => ({
|
||||
...DEFAULT_BUTTON,
|
||||
...btn,
|
||||
labels: { ...(btn.labels || {}) },
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const menuLayoutApi = {
|
||||
getConfig: async (): Promise<MenuConfig> => {
|
||||
const response = await apiClient.get<MenuConfig>('/cabinet/admin/menu-layout');
|
||||
return normalizeConfig(response.data);
|
||||
},
|
||||
|
||||
updateConfig: async (config: MenuConfig): Promise<MenuConfig> => {
|
||||
const response = await apiClient.put<MenuConfig>('/cabinet/admin/menu-layout', config);
|
||||
return normalizeConfig(response.data);
|
||||
},
|
||||
|
||||
resetConfig: async (): Promise<MenuConfig> => {
|
||||
const response = await apiClient.post<MenuConfig>('/cabinet/admin/menu-layout/reset');
|
||||
return normalizeConfig(response.data);
|
||||
},
|
||||
};
|
||||
@@ -49,6 +49,16 @@ export interface UserRoleAssignment {
|
||||
user_telegram_id: number | null;
|
||||
}
|
||||
|
||||
export interface AdminWithRoles {
|
||||
user_id: number;
|
||||
telegram_id: number | null;
|
||||
username: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
email: string | null;
|
||||
role_names: string[];
|
||||
}
|
||||
|
||||
export interface AssignRolePayload {
|
||||
user_id: number;
|
||||
role_id: number;
|
||||
@@ -167,6 +177,13 @@ export const rbacApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- RBAC Users ---
|
||||
|
||||
getRbacUsers: async (): Promise<AdminWithRoles[]> => {
|
||||
const response = await apiClient.get<AdminWithRoles[]>(`${BASE}/users`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- Role Users ---
|
||||
|
||||
getRoleUsers: async (roleId: number): Promise<UserRoleAssignment[]> => {
|
||||
|
||||
@@ -85,6 +85,8 @@ export interface TariffDetail {
|
||||
daily_price_kopeks: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode: string | null; // 'DAY', 'WEEK', 'MONTH', 'NO_RESET', null = глобальная настройка
|
||||
// Внешний сквад RemnaWave
|
||||
external_squad_uuid: string | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -121,6 +123,14 @@ export interface TariffCreateRequest {
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null;
|
||||
// Внешний сквад RemnaWave
|
||||
external_squad_uuid?: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalSquadInfo {
|
||||
uuid: string;
|
||||
name: string;
|
||||
members_count: number;
|
||||
}
|
||||
|
||||
export interface TariffUpdateRequest {
|
||||
@@ -156,6 +166,8 @@ export interface TariffUpdateRequest {
|
||||
daily_price_kopeks?: number;
|
||||
// Режим сброса трафика
|
||||
traffic_reset_mode?: string | null;
|
||||
// Внешний сквад RemnaWave
|
||||
external_squad_uuid?: string | null;
|
||||
}
|
||||
|
||||
export interface TariffToggleResponse {
|
||||
@@ -249,4 +261,10 @@ export const tariffsApi = {
|
||||
const response = await apiClient.get('/cabinet/admin/payment-methods/promo-groups');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get available external squads from RemnaWave
|
||||
getAvailableExternalSquads: async (): Promise<ExternalSquadInfo[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/tariffs/available-external-squads');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user