import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { serversApi, ServerListItem, ServerDetail, ServerUpdateRequest } from '../api/servers' // Icons const BackIcon = () => ( ) const SyncIcon = () => ( ) const EditIcon = () => ( ) const CheckIcon = () => ( ) const XIcon = () => ( ) const UsersIcon = () => ( ) // Country flags (simple emoji mapping) const getCountryFlag = (code: string | null): string => { if (!code) return '' const codeMap: Record = { 'RU': '🇷🇺', 'US': '🇺🇸', 'DE': '🇩🇪', 'NL': '🇳🇱', 'GB': '🇬🇧', 'FR': '🇫🇷', 'FI': '🇫🇮', 'SE': '🇸🇪', 'PL': '🇵🇱', 'CZ': '🇨🇿', 'AT': '🇦🇹', 'CH': '🇨ðŸ‡', 'UA': '🇺🇦', 'KZ': '🇰🇿', 'JP': '🇯🇵', 'KR': '🇰🇷', 'SG': '🇸🇬', 'HK': 'ðŸ‡ðŸ‡°', 'CA': '🇨🇦', 'AU': '🇦🇺', 'BR': '🇧🇷', 'IN': '🇮🇳', 'TR': '🇹🇷', 'IL': '🇮🇱', 'AE': '🇦🇪', } return codeMap[code.toUpperCase()] || code } interface ServerModalProps { server: ServerDetail onSave: (data: ServerUpdateRequest) => void onClose: () => void isLoading?: boolean } function ServerModal({ server, onSave, onClose, isLoading }: ServerModalProps) { const { t } = useTranslation() const [displayName, setDisplayName] = useState(server.display_name) const [description, setDescription] = useState(server.description || '') const [countryCode, setCountryCode] = useState(server.country_code || '') const [priceKopeks, setPriceKopeks] = useState(server.price_kopeks) const [maxUsers, setMaxUsers] = useState(server.max_users) const [sortOrder, setSortOrder] = useState(server.sort_order) const handleSubmit = () => { const data: ServerUpdateRequest = { display_name: displayName, description: description || undefined, country_code: countryCode || undefined, price_kopeks: priceKopeks, max_users: maxUsers || undefined, sort_order: sortOrder, } onSave(data) } return ( {/* Header */} {getCountryFlag(server.country_code)} {t('admin.servers.edit')} {/* Content */} {/* Original Name (readonly) */} {t('admin.servers.originalName')} {server.original_name || server.squad_uuid} {/* Display Name */} {t('admin.servers.displayName')} setDisplayName(e.target.value)} className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500" placeholder={t('admin.servers.displayNamePlaceholder')} /> {/* Description */} {t('admin.servers.description')} setDescription(e.target.value)} className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500 resize-none" rows={2} placeholder={t('admin.servers.descriptionPlaceholder')} /> {/* Country Code */} {t('admin.servers.countryCode')} setCountryCode(e.target.value.toUpperCase().slice(0, 2))} className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500" placeholder="RU" maxLength={2} /> {countryCode && ( {getCountryFlag(countryCode)} )} {/* Price */} {t('admin.servers.price')} setPriceKopeks(Math.max(0, parseFloat(e.target.value) || 0) * 100)} className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500" min={0} step={1} /> ₽ {t('admin.servers.priceHint')} {/* Max Users */} {t('admin.servers.maxUsers')} setMaxUsers(e.target.value ? Math.max(0, parseInt(e.target.value)) : null)} className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500" min={0} placeholder={t('admin.servers.unlimited')} /> {!maxUsers && ( {t('admin.servers.unlimited')} )} {/* Sort Order */} {t('admin.servers.sortOrder')} setSortOrder(parseInt(e.target.value) || 0)} className="w-32 px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-dark-100 focus:outline-none focus:border-accent-500" /> {/* Stats */} {t('admin.servers.stats')} {t('admin.servers.currentUsers')}: {server.current_users} {t('admin.servers.activeSubscriptions')}: {server.active_subscriptions} {server.tariffs_using.length > 0 && ( {t('admin.servers.usedByTariffs')}: {server.tariffs_using.map(tariff => ( {tariff} ))} )} {/* Footer */} {t('common.cancel')} {isLoading ? t('common.loading') : t('common.save')} ) } export default function AdminServers() { const { t } = useTranslation() const queryClient = useQueryClient() const [editingServer, setEditingServer] = useState(null) const [loadingServerId, setLoadingServerId] = useState(null) const [loadError, setLoadError] = useState(null) // Queries const { data: serversData, isLoading } = useQuery({ queryKey: ['admin-servers'], queryFn: () => serversApi.getServers(true), }) // Mutations const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: ServerUpdateRequest }) => serversApi.updateServer(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-servers'] }) setEditingServer(null) }, }) const toggleMutation = useMutation({ mutationFn: serversApi.toggleServer, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-servers'] }) }, }) const toggleTrialMutation = useMutation({ mutationFn: serversApi.toggleTrial, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-servers'] }) }, }) const syncMutation = useMutation({ mutationFn: serversApi.syncServers, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['admin-servers'] }) alert(result.message) }, }) const handleEdit = async (serverId: number) => { setLoadingServerId(serverId) setLoadError(null) try { const detail = await serversApi.getServer(serverId) setEditingServer(detail) } catch (error: unknown) { console.error('Failed to load server:', error) const errorMessage = error instanceof Error ? error.message : t('admin.servers.loadError') setLoadError(errorMessage) } finally { setLoadingServerId(null) } } const handleSave = (data: ServerUpdateRequest) => { if (editingServer) { updateMutation.mutate({ id: editingServer.id, data }) } } const servers = serversData?.servers || [] return ( {/* Header */} {t('admin.servers.title')} {t('admin.servers.subtitle')} syncMutation.mutate()} disabled={syncMutation.isPending} className="flex items-center gap-2 px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-colors disabled:opacity-50" > {syncMutation.isPending ? t('admin.servers.syncing') : t('admin.servers.sync')} {/* Servers List */} {isLoading ? ( ) : servers.length === 0 ? ( {t('admin.servers.noServers')} syncMutation.mutate()} className="mt-4 text-accent-400 hover:text-accent-300" > {t('admin.servers.syncNow')} ) : ( {servers.map((server: ServerListItem) => ( {getCountryFlag(server.country_code)} {server.display_name} {server.is_trial_eligible && ( {t('admin.servers.trial')} )} {!server.is_available && ( {t('admin.servers.unavailable')} )} {server.is_full && ( {t('admin.servers.full')} )} {server.current_users} {server.max_users ? ` / ${server.max_users}` : ''} {server.price_rubles} ₽ {server.squad_uuid} {/* Toggle Available */} toggleMutation.mutate(server.id)} className={`p-2 rounded-lg transition-colors ${ server.is_available ? 'bg-success-500/20 text-success-400 hover:bg-success-500/30' : 'bg-dark-700 text-dark-400 hover:bg-dark-600' }`} title={server.is_available ? t('admin.servers.disable') : t('admin.servers.enable')} > {server.is_available ? : } {/* Toggle Trial */} toggleTrialMutation.mutate(server.id)} className={`p-2 rounded-lg transition-colors ${ server.is_trial_eligible ? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30' : 'bg-dark-700 text-dark-400 hover:bg-dark-600' }`} title={t('admin.servers.toggleTrial')} > T {/* Edit */} handleEdit(server.id)} disabled={loadingServerId === server.id} className="p-2 bg-dark-700 text-dark-300 rounded-lg hover:bg-dark-600 hover:text-dark-100 transition-colors disabled:opacity-50" title={t('admin.servers.edit')} > {loadingServerId === server.id ? ( ) : ( )} ))} )} {/* Error Toast */} {loadError && ( {loadError} setLoadError(null)} className="p-1 hover:bg-white/20 rounded" > )} {/* Edit Modal */} {editingServer && ( setEditingServer(null)} isLoading={updateMutation.isPending} /> )} ) }
{t('admin.servers.priceHint')}
{t('admin.servers.subtitle')}
{t('admin.servers.noServers')}