Add admin features for broadcasts, campaigns, and users

- Introduced new pages and API integrations for managing broadcasts, campaigns, and users in the admin panel.
- Added localization support for new features in English, Persian, Russian, and Chinese.
- Enhanced the AdminPanel to include navigation for the new sections, improving overall admin experience.
This commit is contained in:
PEDZEO
2026-01-17 07:50:17 +03:00
14 changed files with 5284 additions and 63 deletions

168
src/api/adminBroadcasts.ts Normal file
View File

@@ -0,0 +1,168 @@
import apiClient from './client'
// Types
export interface BroadcastFilter {
key: string
label: string
count: number | null
group: string | null
}
export interface TariffFilter {
key: string
label: string
tariff_id: number
count: number
}
export interface BroadcastFiltersResponse {
filters: BroadcastFilter[]
tariff_filters: TariffFilter[]
custom_filters: BroadcastFilter[]
}
export interface TariffForBroadcast {
id: number
name: string
filter_key: string
active_users_count: number
}
export interface BroadcastTariffsResponse {
tariffs: TariffForBroadcast[]
}
export interface BroadcastButton {
key: string
label: string
default: boolean
}
export interface BroadcastButtonsResponse {
buttons: BroadcastButton[]
}
export interface BroadcastMedia {
type: 'photo' | 'video' | 'document'
file_id: string
caption?: string
}
export interface BroadcastCreateRequest {
target: string
message_text: string
selected_buttons: string[]
media?: BroadcastMedia
}
export interface Broadcast {
id: number
target_type: string
message_text: string
has_media: boolean
media_type: string | null
media_file_id: string | null
media_caption: string | null
total_count: number
sent_count: number
failed_count: number
status: 'queued' | 'in_progress' | 'completed' | 'partial' | 'failed' | 'cancelled' | 'cancelling'
admin_id: number | null
admin_name: string | null
created_at: string
completed_at: string | null
progress_percent: number
}
export interface BroadcastListResponse {
items: Broadcast[]
total: number
limit: number
offset: number
}
export interface BroadcastPreviewRequest {
target: string
}
export interface BroadcastPreviewResponse {
target: string
count: number
}
export interface MediaUploadResponse {
media_type: string
file_id: string
file_unique_id: string | null
media_url: string
}
export const adminBroadcastsApi = {
// Get all available filters with counts
getFilters: async (): Promise<BroadcastFiltersResponse> => {
const response = await apiClient.get<BroadcastFiltersResponse>('/cabinet/admin/broadcasts/filters')
return response.data
},
// Get tariffs for filtering
getTariffs: async (): Promise<BroadcastTariffsResponse> => {
const response = await apiClient.get<BroadcastTariffsResponse>('/cabinet/admin/broadcasts/tariffs')
return response.data
},
// Get available buttons
getButtons: async (): Promise<BroadcastButtonsResponse> => {
const response = await apiClient.get<BroadcastButtonsResponse>('/cabinet/admin/broadcasts/buttons')
return response.data
},
// Preview broadcast (get recipients count)
preview: async (target: string): Promise<BroadcastPreviewResponse> => {
const response = await apiClient.post<BroadcastPreviewResponse>('/cabinet/admin/broadcasts/preview', {
target,
})
return response.data
},
// Create and start broadcast
create: async (data: BroadcastCreateRequest): Promise<Broadcast> => {
const response = await apiClient.post<Broadcast>('/cabinet/admin/broadcasts', data)
return response.data
},
// Get list of broadcasts
list: async (limit = 20, offset = 0): Promise<BroadcastListResponse> => {
const response = await apiClient.get<BroadcastListResponse>('/cabinet/admin/broadcasts', {
params: { limit, offset },
})
return response.data
},
// Get broadcast details
get: async (id: number): Promise<Broadcast> => {
const response = await apiClient.get<Broadcast>(`/cabinet/admin/broadcasts/${id}`)
return response.data
},
// Stop broadcast
stop: async (id: number): Promise<Broadcast> => {
const response = await apiClient.post<Broadcast>(`/cabinet/admin/broadcasts/${id}/stop`)
return response.data
},
// Upload media (uses existing media endpoint)
uploadMedia: async (file: File, mediaType: 'photo' | 'video' | 'document'): Promise<MediaUploadResponse> => {
const formData = new FormData()
formData.append('file', file)
formData.append('media_type', mediaType)
const response = await apiClient.post<MediaUploadResponse>('/cabinet/media/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data
},
}
export default adminBroadcastsApi

418
src/api/adminUsers.ts Normal file
View File

@@ -0,0 +1,418 @@
import apiClient from './client'
// ============ Types ============
export interface UserSubscriptionInfo {
id: number
status: string
is_trial: boolean
start_date: string | null
end_date: string | null
traffic_limit_gb: number
traffic_used_gb: number
device_limit: number
tariff_id: number | null
tariff_name: string | null
autopay_enabled: boolean
is_active: boolean
days_remaining: number
}
export interface UserPromoGroupInfo {
id: number
name: string
is_default: boolean
}
export interface UserListItem {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string
status: string
balance_kopeks: number
balance_rubles: number
created_at: string
last_activity: string | null
has_subscription: boolean
subscription_status: string | null
subscription_is_trial: boolean
subscription_end_date: string | null
promo_group_id: number | null
promo_group_name: string | null
total_spent_kopeks: number
purchase_count: number
has_restrictions: boolean
restriction_topup: boolean
restriction_subscription: boolean
}
export interface UsersListResponse {
users: UserListItem[]
total: number
offset: number
limit: number
}
export interface UserTransactionItem {
id: number
type: string
amount_kopeks: number
amount_rubles: number
description: string | null
payment_method: string | null
is_completed: boolean
created_at: string
}
export interface UserReferralInfo {
referral_code: string
referrals_count: number
total_earnings_kopeks: number
commission_percent: number | null
referred_by_id: number | null
referred_by_username: string | null
}
export interface UserDetailResponse {
id: number
telegram_id: number
username: string | null
first_name: string | null
last_name: string | null
full_name: string
status: string
language: string
balance_kopeks: number
balance_rubles: number
email: string | null
email_verified: boolean
created_at: string
updated_at: string | null
last_activity: string | null
cabinet_last_login: string | null
subscription: UserSubscriptionInfo | null
promo_group: UserPromoGroupInfo | null
referral: UserReferralInfo
total_spent_kopeks: number
purchase_count: number
used_promocodes: number
has_had_paid_subscription: boolean
lifetime_used_traffic_bytes: number
restriction_topup: boolean
restriction_subscription: boolean
restriction_reason: string | null
promo_offer_discount_percent: number
promo_offer_discount_source: string | null
promo_offer_discount_expires_at: string | null
recent_transactions: UserTransactionItem[]
remnawave_uuid: string | null
}
export interface UsersStatsResponse {
total_users: number
active_users: number
blocked_users: number
deleted_users: number
new_today: number
new_week: number
new_month: number
users_with_subscription: number
users_with_active_subscription: number
users_with_trial: number
users_with_expired_subscription: number
total_balance_kopeks: number
total_balance_rubles: number
avg_balance_kopeks: number
active_today: number
active_week: number
active_month: number
}
// Available tariffs
export interface PeriodPriceInfo {
days: number
price_kopeks: number
price_rubles: number
}
export interface UserAvailableTariff {
id: number
name: string
description: string | null
is_active: boolean
is_trial_available: boolean
traffic_limit_gb: number
device_limit: number
tier_level: number
display_order: number
period_prices: PeriodPriceInfo[]
is_daily: boolean
daily_price_kopeks: number
custom_days_enabled: boolean
price_per_day_kopeks: number
min_days: number
max_days: number
is_available: boolean
requires_promo_group: boolean
}
export interface UserAvailableTariffsResponse {
user_id: number
promo_group_id: number | null
promo_group_name: string | null
tariffs: UserAvailableTariff[]
total: number
current_tariff_id: number | null
current_tariff_name: string | null
}
// Sync types
export interface PanelUserInfo {
uuid: string | null
short_uuid: string | null
username: string | null
status: string | null
expire_at: string | null
traffic_limit_gb: number
traffic_used_gb: number
device_limit: number
subscription_url: string | null
active_squads: string[]
}
export interface SyncFromPanelResponse {
success: boolean
message: string
panel_user: PanelUserInfo | null
changes: Record<string, unknown>
errors: string[]
}
export interface SyncToPanelResponse {
success: boolean
message: string
action: string
panel_uuid: string | null
changes: Record<string, unknown>
errors: string[]
}
export interface PanelSyncStatusResponse {
user_id: number
telegram_id: number
remnawave_uuid: string | null
last_sync: string | null
bot_subscription_status: string | null
bot_subscription_end_date: string | null
bot_traffic_limit_gb: number
bot_traffic_used_gb: number
bot_device_limit: number
bot_squads: string[]
panel_found: boolean
panel_status: string | null
panel_expire_at: string | null
panel_traffic_limit_gb: number
panel_traffic_used_gb: number
panel_device_limit: number
panel_squads: string[]
has_differences: boolean
differences: string[]
}
// Update types
export interface UpdateBalanceRequest {
amount_kopeks: number
description?: string
create_transaction?: boolean
}
export interface UpdateBalanceResponse {
success: boolean
old_balance_kopeks: number
new_balance_kopeks: number
message: string
}
export interface UpdateSubscriptionRequest {
action: 'extend' | 'set_end_date' | 'change_tariff' | 'set_traffic' | 'toggle_autopay' | 'cancel' | 'activate' | 'create'
days?: number
end_date?: string
tariff_id?: number
traffic_limit_gb?: number
traffic_used_gb?: number
autopay_enabled?: boolean
is_trial?: boolean
device_limit?: number
}
export interface UpdateSubscriptionResponse {
success: boolean
message: string
subscription: UserSubscriptionInfo | null
}
export interface UpdateUserStatusResponse {
success: boolean
old_status: string
new_status: string
message: string
}
export interface UpdateRestrictionsRequest {
restriction_topup?: boolean
restriction_subscription?: boolean
restriction_reason?: string
}
export interface UpdateRestrictionsResponse {
success: boolean
restriction_topup: boolean
restriction_subscription: boolean
restriction_reason: string | null
message: string
}
export interface SyncFromPanelRequest {
update_subscription?: boolean
update_traffic?: boolean
create_if_missing?: boolean
}
export interface SyncToPanelRequest {
create_if_missing?: boolean
update_status?: boolean
update_traffic_limit?: boolean
update_expire_date?: boolean
update_squads?: boolean
}
// ============ API ============
export const adminUsersApi = {
// List users
getUsers: async (params: {
offset?: number
limit?: number
search?: string
status?: 'active' | 'blocked' | 'deleted'
sort_by?: 'created_at' | 'balance' | 'traffic' | 'last_activity' | 'total_spent' | 'purchase_count'
} = {}): Promise<UsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/users', { params })
return response.data
},
// Get users stats
getStats: async (): Promise<UsersStatsResponse> => {
const response = await apiClient.get('/cabinet/admin/users/stats')
return response.data
},
// Get user detail
getUser: async (userId: number): Promise<UserDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}`)
return response.data
},
// Get user by telegram ID
getUserByTelegram: async (telegramId: number): Promise<UserDetailResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/by-telegram/${telegramId}`)
return response.data
},
// Get available tariffs for user
getAvailableTariffs: async (userId: number, includeInactive = false): Promise<UserAvailableTariffsResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/available-tariffs`, {
params: { include_inactive: includeInactive }
})
return response.data
},
// Update balance
updateBalance: async (userId: number, data: UpdateBalanceRequest): Promise<UpdateBalanceResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/balance`, data)
return response.data
},
// Update subscription
updateSubscription: async (userId: number, data: UpdateSubscriptionRequest): Promise<UpdateSubscriptionResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/subscription`, data)
return response.data
},
// Update status
updateStatus: async (userId: number, status: 'active' | 'blocked' | 'deleted', reason?: string): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/status`, { status, reason })
return response.data
},
// Block user
blockUser: async (userId: number, reason?: string): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/block`, null, { params: { reason } })
return response.data
},
// Unblock user
unblockUser: async (userId: number): Promise<UpdateUserStatusResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/unblock`)
return response.data
},
// Update restrictions
updateRestrictions: async (userId: number, data: UpdateRestrictionsRequest): Promise<UpdateRestrictionsResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/restrictions`, data)
return response.data
},
// Update promo group
updatePromoGroup: async (userId: number, promoGroupId: number | null): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/promo-group`, { promo_group_id: promoGroupId })
return response.data
},
// Delete user
deleteUser: async (userId: number, softDelete = true, reason?: string): Promise<{ success: boolean; message: string }> => {
const response = await apiClient.delete(`/cabinet/admin/users/${userId}`, {
data: { soft_delete: softDelete, reason }
})
return response.data
},
// Get referrals
getReferrals: async (userId: number, offset = 0, limit = 50): Promise<UsersListResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, {
params: { offset, limit }
})
return response.data
},
// Get transactions
getTransactions: async (userId: number, params: {
offset?: number
limit?: number
transaction_type?: string
} = {}): Promise<{ transactions: UserTransactionItem[]; total: number; offset: number; limit: number }> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/transactions`, { params })
return response.data
},
// Sync status
getSyncStatus: async (userId: number): Promise<PanelSyncStatusResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`)
return response.data
},
// Sync from panel
syncFromPanel: async (userId: number, data: SyncFromPanelRequest = {}): Promise<SyncFromPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data)
return response.data
},
// Sync to panel
syncToPanel: async (userId: number, data: SyncToPanelRequest = {}): Promise<SyncToPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data)
return response.data
},
}

230
src/api/campaigns.ts Normal file
View File

@@ -0,0 +1,230 @@
import apiClient from './client'
// Types
export type CampaignBonusType = 'balance' | 'subscription' | 'none' | 'tariff'
export interface TariffInfo {
id: number
name: string
}
export interface CampaignListItem {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
registrations_count: number
total_revenue_kopeks: number
conversion_rate: number
created_at: string
}
export interface CampaignListResponse {
campaigns: CampaignListItem[]
total: number
}
export interface CampaignDetail {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
balance_bonus_kopeks: number
balance_bonus_rubles: number
subscription_duration_days: number | null
subscription_traffic_gb: number | null
subscription_device_limit: number | null
subscription_squads: string[]
tariff_id: number | null
tariff_duration_days: number | null
tariff: TariffInfo | null
created_by: number | null
created_at: string
updated_at: string | null
deep_link: string | null
}
export interface CampaignCreateRequest {
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active?: boolean
balance_bonus_kopeks?: number
subscription_duration_days?: number
subscription_traffic_gb?: number
subscription_device_limit?: number
subscription_squads?: string[]
tariff_id?: number
tariff_duration_days?: number
}
export interface CampaignUpdateRequest {
name?: string
start_parameter?: string
bonus_type?: CampaignBonusType
is_active?: boolean
balance_bonus_kopeks?: number
subscription_duration_days?: number
subscription_traffic_gb?: number
subscription_device_limit?: number
subscription_squads?: string[]
tariff_id?: number
tariff_duration_days?: number
}
export interface CampaignToggleResponse {
id: number
is_active: boolean
message: string
}
export interface CampaignStatistics {
id: number
name: string
start_parameter: string
bonus_type: CampaignBonusType
is_active: boolean
registrations: number
balance_issued_kopeks: number
balance_issued_rubles: number
subscription_issued: number
last_registration: string | null
total_revenue_kopeks: number
total_revenue_rubles: number
avg_revenue_per_user_kopeks: number
avg_revenue_per_user_rubles: number
avg_first_payment_kopeks: number
avg_first_payment_rubles: number
trial_users_count: number
active_trials_count: number
conversion_count: number
paid_users_count: number
conversion_rate: number
trial_conversion_rate: number
deep_link: string | null
}
export interface CampaignRegistrationItem {
id: number
user_id: number
telegram_id: number
username: string | null
first_name: string | null
bonus_type: string
balance_bonus_kopeks: number
subscription_duration_days: number | null
tariff_id: number | null
tariff_duration_days: number | null
created_at: string
user_balance_kopeks: number
has_subscription: boolean
has_paid: boolean
}
export interface CampaignRegistrationsResponse {
registrations: CampaignRegistrationItem[]
total: number
page: number
per_page: number
}
export interface CampaignsOverview {
total: number
active: number
inactive: number
total_registrations: number
total_balance_issued_kopeks: number
total_balance_issued_rubles: number
total_subscription_issued: number
total_tariff_issued: number
}
export interface ServerSquadInfo {
id: number
squad_uuid: string
display_name: string
country_code: string | null
}
export interface TariffListItem {
id: number
name: string
description: string | null
is_active: boolean
traffic_limit_gb: number
device_limit: number
}
export const campaignsApi = {
// Get campaigns overview
getOverview: async (): Promise<CampaignsOverview> => {
const response = await apiClient.get('/cabinet/admin/campaigns/overview')
return response.data
},
// Get all campaigns
getCampaigns: async (includeInactive = true, offset = 0, limit = 50): Promise<CampaignListResponse> => {
const response = await apiClient.get('/cabinet/admin/campaigns', {
params: { include_inactive: includeInactive, offset, limit }
})
return response.data
},
// Get single campaign
getCampaign: async (campaignId: number): Promise<CampaignDetail> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}`)
return response.data
},
// Get campaign statistics
getCampaignStats: async (campaignId: number): Promise<CampaignStatistics> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/stats`)
return response.data
},
// Get campaign registrations
getCampaignRegistrations: async (campaignId: number, page = 1, perPage = 50): Promise<CampaignRegistrationsResponse> => {
const response = await apiClient.get(`/cabinet/admin/campaigns/${campaignId}/registrations`, {
params: { page, per_page: perPage }
})
return response.data
},
// Create campaign
createCampaign: async (data: CampaignCreateRequest): Promise<CampaignDetail> => {
const response = await apiClient.post('/cabinet/admin/campaigns', data)
return response.data
},
// Update campaign
updateCampaign: async (campaignId: number, data: CampaignUpdateRequest): Promise<CampaignDetail> => {
const response = await apiClient.put(`/cabinet/admin/campaigns/${campaignId}`, data)
return response.data
},
// Delete campaign
deleteCampaign: async (campaignId: number): Promise<{ message: string }> => {
const response = await apiClient.delete(`/cabinet/admin/campaigns/${campaignId}`)
return response.data
},
// Toggle campaign active status
toggleCampaign: async (campaignId: number): Promise<CampaignToggleResponse> => {
const response = await apiClient.post(`/cabinet/admin/campaigns/${campaignId}/toggle`)
return response.data
},
// Get available servers for subscription bonus
getAvailableServers: async (): Promise<ServerSquadInfo[]> => {
const response = await apiClient.get('/cabinet/admin/campaigns/available-servers')
return response.data
},
// Get available tariffs for tariff bonus
getAvailableTariffs: async (): Promise<TariffListItem[]> => {
const response = await apiClient.get('/cabinet/admin/campaigns/available-tariffs')
return response.data
},
}

181
src/api/promocodes.ts Normal file
View File

@@ -0,0 +1,181 @@
import apiClient from './client'
// ============== Types ==============
export type PromoCodeType = 'balance' | 'subscription_days' | 'trial_subscription' | 'promo_group'
export interface PromoCode {
id: number
code: string
type: PromoCodeType
balance_bonus_kopeks: number
balance_bonus_rubles: number
subscription_days: number
max_uses: number
current_uses: number
uses_left: number
is_active: boolean
is_valid: boolean
first_purchase_only: boolean
valid_from: string
valid_until: string | null
promo_group_id: number | null
created_by: number | null
created_at: string
updated_at: string
}
export interface PromoCodeRecentUse {
id: number
user_id: number
user_username: string | null
user_full_name: string | null
user_telegram_id: number | null
used_at: string
}
export interface PromoCodeDetail extends PromoCode {
total_uses: number
today_uses: number
recent_uses: PromoCodeRecentUse[]
}
export interface PromoCodeListResponse {
items: PromoCode[]
total: number
limit: number
offset: number
}
export interface PromoCodeCreateRequest {
code: string
type: PromoCodeType
balance_bonus_kopeks?: number
subscription_days?: number
max_uses?: number
valid_from?: string
valid_until?: string | null
is_active?: boolean
first_purchase_only?: boolean
promo_group_id?: number | null
}
export interface PromoCodeUpdateRequest {
code?: string
type?: PromoCodeType
balance_bonus_kopeks?: number
subscription_days?: number
max_uses?: number
valid_from?: string
valid_until?: string | null
is_active?: boolean
first_purchase_only?: boolean
promo_group_id?: number | null
}
// ============== PromoGroup Types ==============
export interface PromoGroup {
id: number
name: string
server_discount_percent: number
traffic_discount_percent: number
device_discount_percent: number
period_discounts: Record<number, number>
auto_assign_total_spent_kopeks: number | null
apply_discounts_to_addons: boolean
is_default: boolean
members_count: number
created_at: string | null
updated_at: string | null
}
export interface PromoGroupListResponse {
items: PromoGroup[]
total: number
limit: number
offset: number
}
export interface PromoGroupCreateRequest {
name: string
server_discount_percent?: number
traffic_discount_percent?: number
device_discount_percent?: number
period_discounts?: Record<number, number>
auto_assign_total_spent_kopeks?: number | null
apply_discounts_to_addons?: boolean
is_default?: boolean
}
export interface PromoGroupUpdateRequest {
name?: string
server_discount_percent?: number
traffic_discount_percent?: number
device_discount_percent?: number
period_discounts?: Record<number, number>
auto_assign_total_spent_kopeks?: number | null
apply_discounts_to_addons?: boolean
is_default?: boolean
}
// ============== API ==============
export const promocodesApi = {
// Promocodes
getPromocodes: async (params?: {
limit?: number
offset?: number
is_active?: boolean
}): Promise<PromoCodeListResponse> => {
const response = await apiClient.get('/cabinet/admin/promocodes', { params })
return response.data
},
getPromocode: async (id: number): Promise<PromoCodeDetail> => {
const response = await apiClient.get(`/cabinet/admin/promocodes/${id}`)
return response.data
},
createPromocode: async (data: PromoCodeCreateRequest): Promise<PromoCode> => {
const response = await apiClient.post('/cabinet/admin/promocodes', data)
return response.data
},
updatePromocode: async (id: number, data: PromoCodeUpdateRequest): Promise<PromoCode> => {
const response = await apiClient.patch(`/cabinet/admin/promocodes/${id}`, data)
return response.data
},
deletePromocode: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/promocodes/${id}`)
},
// Promo Groups
getPromoGroups: async (params?: {
limit?: number
offset?: number
}): Promise<PromoGroupListResponse> => {
const response = await apiClient.get('/cabinet/admin/promo-groups', { params })
return response.data
},
getPromoGroup: async (id: number): Promise<PromoGroup> => {
const response = await apiClient.get(`/cabinet/admin/promo-groups/${id}`)
return response.data
},
createPromoGroup: async (data: PromoGroupCreateRequest): Promise<PromoGroup> => {
const response = await apiClient.post('/cabinet/admin/promo-groups', data)
return response.data
},
updatePromoGroup: async (id: number, data: PromoGroupUpdateRequest): Promise<PromoGroup> => {
const response = await apiClient.patch(`/cabinet/admin/promo-groups/${id}`, data)
return response.data
},
deletePromoGroup: async (id: number): Promise<void> => {
await apiClient.delete(`/cabinet/admin/promo-groups/${id}`)
},
}