Files
bedolaga-cabinet/src/types/index.ts
Fringg 321c65b68b feat(devices): inline rename UI for connected HWID devices
Pairs with the bot commit that adds the user_device_aliases table.
Surfaces the new local_name field on the existing devices list in
both the user-facing subscription page and the admin user-detail
page, with the same inline-edit pattern in both places.

User-side (src/pages/Subscription.tsx):
- pencil button next to each device row toggles edit mode
- input is focused automatically, capped at 64 chars (matches the
  backend ALIAS_MAX_LENGTH and DB column width)
- Enter saves, Escape cancels, empty input clears the alias
- display priority: local_name → device_model → platform
- works identically in classic / single-tariff / multi-tariff —
  subscriptionId is forwarded as a query param like every other
  device-management endpoint already does

Admin-side (src/pages/AdminUserDetail.tsx):
- same pencil + inline input pattern, admin acts on behalf of the
  user. notify.success on save, loadDevices() refresh.

API (src/api/subscription.ts + src/api/adminUsers.ts):
- new renameDevice(hwid, name, subscriptionId?) on subscriptionApi
- new renameUserDevice(userId, hwid, name) on adminUsersApi
- existing getDevices/getUserDevices contracts widened with
  local_name?: string | null on the returned device shape

Locales (src/locales/ru.json):
- subscription.renameDevice / .renameDeviceSave / .renameDeviceCancel
  / .renameDevicePlaceholder / .deviceRenamed
- admin.users.detail.devices.rename / .renameSave / .renamed
2026-05-16 03:02:45 +03:00

762 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// User types
export interface User {
id: number;
telegram_id: number | null; // Nullable для email-only пользователей
username: string | null;
first_name: string | null;
last_name: string | null;
email: string | null;
email_verified: boolean;
balance_kopeks: number;
balance_rubles: number;
referral_code: string | null;
language: string;
created_at: string;
auth_type: 'telegram' | 'email' | 'google' | 'yandex' | 'discord' | 'vk'; // Тип аутентификации
}
// OAuth types
export interface OAuthProvider {
name: string;
display_name: string;
}
// Campaign bonus info (returned during auth)
export interface CampaignBonusInfo {
campaign_name: string;
bonus_type: 'balance' | 'subscription' | 'tariff' | 'none';
balance_kopeks: number;
subscription_days: number | null;
tariff_name: string | null;
}
// Auth types
export interface AuthResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
user: User;
campaign_bonus?: CampaignBonusInfo | null;
}
export interface TokenResponse {
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
}
export interface RegisterResponse {
message: string;
email: string;
requires_verification: boolean;
}
// Subscription types
export interface ServerInfo {
uuid: string;
name: string;
country_code: string | null;
}
export interface TrafficPurchase {
id: number;
traffic_gb: number;
expires_at: string;
created_at: string;
days_remaining: number;
progress_percent: number;
}
export interface Subscription {
id: number;
status: string;
is_trial: boolean;
start_date: string;
end_date: string;
days_left: number;
hours_left: number;
minutes_left: number;
time_left_display: string;
traffic_limit_gb: number;
traffic_used_gb: number;
traffic_used_percent: number;
device_limit: number;
connected_squads: string[];
servers: ServerInfo[];
autopay_enabled: boolean;
autopay_days_before: number;
subscription_url: string | null;
hide_subscription_link: boolean;
is_active: boolean;
is_expired: boolean;
is_limited: boolean;
traffic_purchases?: TrafficPurchase[];
// Daily tariff fields
is_daily?: boolean;
is_daily_paused?: boolean;
daily_price_kopeks?: number;
next_daily_charge_at?: string; // ISO datetime string
// Tariff info
tariff_id?: number;
tariff_name?: string;
traffic_reset_mode?: string;
}
// Response wrapper for subscription status endpoint
export interface SubscriptionStatusResponse {
has_subscription: boolean;
subscription: Subscription | null;
}
// Multi-tariff subscription list item (from GET /cabinet/subscriptions)
export interface SubscriptionListItem {
id: number;
status: string;
tariff_id: number | null;
tariff_name: string | null;
traffic_limit_gb: number;
traffic_used_gb: number;
device_limit: number;
end_date: string | null;
subscription_url: string | null;
subscription_crypto_link: string | null;
is_trial: boolean;
is_daily?: boolean;
is_daily_paused?: boolean;
autopay_enabled: boolean;
connected_squads: string[] | null;
}
// Response from GET /cabinet/subscriptions (multi-tariff)
export interface SubscriptionsListResponse {
subscriptions: SubscriptionListItem[];
multi_tariff_enabled: boolean;
}
// Device types
export interface Device {
hwid: string;
platform: string;
device_model: string;
created_at: string | null;
/**
* User-set local alias persisted in the bot DB (`user_device_aliases`).
* `null` when the user hasn't renamed the device — clients fall back
* to `device_model` / `platform` for display.
*/
local_name?: string | null;
}
export interface DevicesResponse {
devices: Device[];
total: number;
device_limit: number;
}
// Tariff switch preview
export interface TariffSwitchPreview {
can_switch: boolean;
current_tariff_id: number | null;
current_tariff_name: string | null;
new_tariff_id: number;
new_tariff_name: string;
remaining_days: number;
upgrade_cost_kopeks: number;
upgrade_cost_label: string;
balance_kopeks: number;
balance_label: string;
has_enough_balance: boolean;
missing_amount_kopeks: number;
missing_amount_label: string;
is_upgrade: boolean;
// Discount fields (from promo group)
base_upgrade_cost_kopeks?: number;
discount_percent?: number;
discount_kopeks?: number;
}
export interface RenewalOption {
period_days: number;
price_kopeks: number;
price_rubles: number;
discount_percent: number;
original_price_kopeks: number | null;
}
export interface TrafficPackage {
gb: number;
price_kopeks: number;
price_rubles: number;
is_unlimited: boolean;
// Discount fields (from promo group)
base_price_kopeks?: number;
discount_percent?: number;
discount_kopeks?: number;
}
export interface TrialInfo {
is_available: boolean;
duration_days: number;
traffic_limit_gb: number;
device_limit: number;
requires_payment: boolean;
price_kopeks: number;
price_rubles: number;
reason_unavailable: string | null;
}
// Purchase options types
export interface TrafficOption {
value: number;
label: string;
price_kopeks: number;
price_label: string;
original_price_kopeks?: number;
original_price_label?: string;
discount_percent?: number;
is_available: boolean;
is_default?: boolean;
}
export interface ServerOption {
uuid: string;
name: string;
price_kopeks: number;
price_label: string;
original_price_kopeks?: number;
original_price_label?: string;
discount_percent?: number;
is_available: boolean;
}
export interface DevicesConfig {
min: number;
max: number;
default: number;
current: number;
price_per_device_kopeks: number;
price_per_device_label: string;
price_per_device_original_kopeks?: number;
discount_percent?: number;
}
export interface TrafficConfig {
selectable: boolean;
mode: string;
options: TrafficOption[];
default?: number;
current?: number;
}
export interface ServersConfig {
options: ServerOption[];
min: number;
max: number;
default: string[];
selected: string[];
}
export interface PeriodOption {
id: string;
period_days: number;
months: number;
label: string;
price_kopeks: number;
price_label: string;
per_month_price_kopeks: number;
per_month_price_label: string;
discount_percent?: number;
original_price_kopeks?: number;
original_price_label?: string;
is_available: boolean;
traffic: TrafficConfig;
servers: ServersConfig;
devices: DevicesConfig;
}
// Tariff types for tariffs mode
export interface TariffPeriod {
days: number;
months: number;
label: string;
price_kopeks: number;
price_label: string;
price_per_month_kopeks: number;
price_per_month_label: string;
// Discount info (if promo group discount applied)
original_price_kopeks?: number;
original_price_label?: string;
original_per_month_kopeks?: number;
original_per_month_label?: string;
discount_percent?: number;
discount_amount_kopeks?: number;
discount_label?: string;
// Extra devices info (additional devices beyond tariff base)
extra_devices_count?: number;
extra_devices_cost_kopeks?: number;
extra_devices_cost_label?: string;
base_tariff_price_kopeks?: number;
base_tariff_price_label?: string;
}
export interface TariffServer {
uuid: string;
name: string;
}
export interface Tariff {
id: number;
name: string;
description: string | null;
tier_level: number;
traffic_limit_gb: number;
traffic_limit_label: string;
is_unlimited_traffic: boolean;
device_limit: number;
base_device_limit?: number;
extra_devices_count: number;
servers_count: number;
servers: TariffServer[];
periods: TariffPeriod[];
is_current: boolean;
is_available: boolean;
// Custom days options
custom_days_enabled?: boolean;
price_per_day_kopeks?: number;
min_days?: number;
max_days?: number;
// Custom traffic options
custom_traffic_enabled?: boolean;
traffic_price_per_gb_kopeks?: number;
min_traffic_gb?: number;
max_traffic_gb?: number;
// Device price
device_price_kopeks?: number;
// Traffic topup options
traffic_topup_enabled?: boolean;
traffic_topup_packages?: number[];
max_topup_traffic_gb?: number;
// Daily tariff options
is_daily?: boolean;
daily_price_kopeks?: number;
// Promo group discount info
promo_group_name?: string;
original_device_price_kopeks?: number;
device_discount_percent?: number;
original_daily_price_kopeks?: number;
daily_discount_percent?: number;
original_price_per_day_kopeks?: number;
custom_days_discount_percent?: number;
// Traffic reset
traffic_reset_mode?: string;
// Multi-tariff: already purchased by user
is_purchased?: boolean;
}
export interface TariffsPurchaseOptions {
sales_mode: 'tariffs';
tariffs: Tariff[];
current_tariff_id: number | null;
balance_kopeks: number;
balance_label: string;
// New fields for expired subscription handling
subscription_status?: string;
subscription_is_expired?: boolean;
has_subscription?: boolean;
// Multi-tariff: all available tariffs already purchased
all_tariffs_purchased?: boolean;
}
export interface ClassicPurchaseOptions {
sales_mode: 'classic';
currency: string;
balance_kopeks: number;
balance_label: string;
subscription_id: number | null;
periods: PeriodOption[];
traffic: TrafficConfig;
servers: ServersConfig;
devices: DevicesConfig;
selection: {
period_id: string;
period_days: number;
traffic_value: number;
servers: string[];
devices: number;
};
}
export type PurchaseOptions = TariffsPurchaseOptions | ClassicPurchaseOptions;
export interface PurchaseSelection {
period_id?: string;
period_days?: number;
traffic_value?: number;
servers?: string[];
devices?: number;
}
export interface PurchasePreview {
total_price_kopeks: number;
total_price_label: string;
original_price_kopeks?: number;
original_price_label?: string;
discount_percent?: number;
discount_label?: string;
per_month_price_kopeks: number;
per_month_price_label: string;
breakdown: { label: string; value: string }[];
balance_kopeks: number;
balance_label: string;
missing_amount_kopeks: number;
missing_amount_label?: string;
can_purchase: boolean;
status_message?: string;
}
// Balance types
export interface Balance {
balance_kopeks: number;
balance_rubles: number;
}
export interface Transaction {
id: number;
type: string;
amount_kopeks: number;
amount_rubles: number;
description: string | null;
payment_method: string | null;
is_completed: boolean;
created_at: string;
completed_at: string | null;
}
export interface PaymentMethodOption {
id: string;
name: string;
description?: string | null;
}
export interface PaymentMethod {
id: string;
name: string;
description: string | null;
min_amount_kopeks: number;
max_amount_kopeks: number;
is_available: boolean;
options?: PaymentMethodOption[] | null;
// Если true — после получения payment_url кабинет сразу делает
// window.location.href вместо показа панели с кнопкой "Открыть".
open_url_direct?: boolean;
}
// Referral types
export interface ReferralInfo {
referral_code: string;
referral_link: string;
bot_referral_link?: string;
total_referrals: number;
active_referrals: number;
total_earnings_kopeks: number;
total_earnings_rubles: number;
commission_percent: number;
available_balance_kopeks: number;
available_balance_rubles: number;
withdrawn_kopeks: number;
}
export interface ReferralTerms {
is_enabled: boolean;
commission_percent: number;
minimum_topup_kopeks: number;
minimum_topup_rubles: number;
first_topup_bonus_kopeks: number;
first_topup_bonus_rubles: number;
inviter_bonus_kopeks: number;
inviter_bonus_rubles: number;
max_commission_payments: number;
partner_section_visible?: boolean;
}
// Ticket types
export interface TicketMediaItem {
type: 'photo' | 'video' | 'document';
file_id: string;
caption?: string | null;
}
export interface TicketMessage {
id: number;
message_text: string;
is_from_admin: boolean;
has_media: boolean;
media_type: string | null;
media_file_id: string | null;
media_caption: string | null;
media_items?: TicketMediaItem[] | null;
created_at: string;
}
export interface Ticket {
id: number;
title: string;
status: string;
priority: string;
created_at: string;
updated_at: string;
closed_at: string | null;
messages_count: number;
last_message: TicketMessage | null;
}
export interface TicketDetail extends Omit<Ticket, 'messages_count' | 'last_message'> {
is_reply_blocked: boolean;
messages: TicketMessage[];
}
export interface SupportConfig {
tickets_enabled: boolean;
support_type: 'tickets' | 'profile' | 'url' | 'both';
support_url?: string | null;
support_username?: string | null;
}
// Paginated response
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
per_page: number;
pages: number;
}
// App config types (for connection setup)
export interface LocalizedText {
[key: string]: string;
}
// RemnaWave format types
export interface RemnawaveButtonClient {
url?: string;
link?: string;
text: LocalizedText;
type?: 'external' | 'subscriptionLink' | 'copyButton';
svgIconKey?: string;
resolvedUrl?: string;
}
export interface RemnawaveBlockClient {
title: LocalizedText;
description: LocalizedText;
buttons?: RemnawaveButtonClient[];
svgIconKey?: string;
svgIconColor?: string;
}
export interface RemnawaveAppClient {
name: string;
featured?: boolean;
deepLink?: string | null;
svgIconKey?: string;
blocks: RemnawaveBlockClient[];
}
export interface RemnawavePlatformData {
svgIconKey?: string;
displayName?: LocalizedText;
apps: RemnawaveAppClient[];
}
export interface AppConfig {
platformNames: Record<string, LocalizedText>;
hasSubscription: boolean;
subscriptionUrl: string | null;
hideLink?: boolean;
branding?: {
name?: string;
logoUrl?: string;
supportUrl?: string;
};
// RemnaWave
isRemnawave?: boolean;
svgLibrary?: Record<string, string | { svgString: string }>;
baseTranslations?: Record<string, LocalizedText>;
baseSettings?: { isShowTutorialButton: boolean; tutorialUrl: string };
uiConfig?: {
installationGuidesBlockType?: 'cards' | 'timeline' | 'accordion' | 'minimal';
};
platforms: Record<string, RemnawavePlatformData>;
}
// Pending payment types
export interface PendingPayment {
id: number;
method: string;
method_display: string;
identifier: string;
amount_kopeks: number;
amount_rubles: number;
status: string;
status_emoji: string;
status_text: string;
is_paid: boolean;
is_checkable: boolean;
created_at: string;
expires_at: string | null;
payment_url: string | null;
user_id?: number;
user_telegram_id?: number;
user_username?: string | null;
user_email?: string | null;
}
export interface ManualCheckResponse {
success: boolean;
message: string;
payment: PendingPayment | null;
status_changed: boolean;
old_status: string | null;
new_status: string | null;
}
// Saved payment method (card) for recurrent payments
export interface SavedCard {
id: number;
method_type: string;
card_last4: string | null;
card_type: string | null;
title: string | null;
created_at: string;
}
export interface SavedCardsResponse {
cards: SavedCard[];
recurrent_enabled: boolean;
}
// Ticket notifications types
export interface TicketNotification {
id: number;
ticket_id: number;
notification_type: 'new_ticket' | 'admin_reply' | 'user_reply';
message: string | null;
is_read: boolean;
created_at: string;
read_at: string | null;
}
export interface TicketNotificationList {
items: TicketNotification[];
unread_count: number;
}
export interface UnreadCountResponse {
unread_count: number;
}
export interface TicketSettings {
sla_enabled: boolean;
sla_minutes: number;
sla_check_interval_seconds: number;
sla_reminder_cooldown_minutes: number;
support_system_mode: string;
cabinet_user_notifications_enabled: boolean;
cabinet_admin_notifications_enabled: boolean;
}
// Payment method config types (admin)
export interface PaymentMethodSubOptionInfo {
id: string;
name: string;
}
export interface PaymentMethodConfig {
method_id: string;
sort_order: number;
is_enabled: boolean;
display_name: string | null;
default_display_name: string;
sub_options: Record<string, boolean> | null;
available_sub_options: PaymentMethodSubOptionInfo[] | null;
min_amount_kopeks: number | null;
max_amount_kopeks: number | null;
default_min_amount_kopeks: number;
default_max_amount_kopeks: number;
user_type_filter: 'all' | 'telegram' | 'email';
first_topup_filter: 'any' | 'yes' | 'no';
promo_group_filter_mode: 'all' | 'selected';
allowed_promo_group_ids: number[];
open_url_direct: boolean;
is_provider_configured: boolean;
created_at: string | null;
updated_at: string | null;
}
export interface PromoGroupSimple {
id: number;
name: string;
}
// Account Linking
export interface LinkedProvider {
provider: string;
linked: boolean;
identifier: string | null;
}
export interface LinkedProvidersResponse {
providers: LinkedProvider[];
}
export interface LinkCallbackResponse {
success: boolean;
message: string | null;
merge_required: boolean;
merge_token: string | null;
}
export interface ServerCompleteResponse extends LinkCallbackResponse {
provider: string;
}
// Account Merge
export interface MergeSubscriptionPreview {
status: string;
is_trial: boolean;
end_date: string | null;
traffic_limit_gb: number;
traffic_used_gb: number;
device_limit: number;
tariff_name: string | null;
autopay_enabled: boolean;
}
export interface MergeAccountPreview {
id: number;
username: string | null;
first_name: string | null;
email: string | null;
auth_methods: string[];
balance_kopeks: number;
subscription: MergeSubscriptionPreview | null;
created_at: string | null;
}
export interface MergePreviewResponse {
primary: MergeAccountPreview;
secondary: MergeAccountPreview;
expires_in_seconds: number;
}
export interface MergeResponse {
success: boolean;
access_token: string | null;
refresh_token: string | null;
user: User | null;
}