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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" placeholder={t('admin.servers.displayNamePlaceholder')} /> {/* Description */} {t('admin.servers.description')} setDescription(e.target.value)} className="w-full resize-none rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" 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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" 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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" 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 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" /> {/* 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 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 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={`rounded-lg p-2 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={`rounded-lg p-2 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="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100 disabled:opacity-50" title={t('admin.servers.edit')} > {loadingServerId === server.id ? ( ) : ( )} ))} )} {/* Error Toast */} {loadError && ( {loadError} setLoadError(null)} className="rounded p-1 hover:bg-white/20"> )} {/* Edit Modal */} {editingServer && ( setEditingServer(null)} isLoading={updateMutation.isPending} /> )} ); }
{t('admin.servers.priceHint')}
{t('admin.servers.subtitle')}
{t('admin.servers.noServers')}