import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { fetchGuildAudit, fetchGuildBans, guildAuditQueryKey, guildBansQueryKey, timeoutGuildMember, unbanGuildMember, type AuditFilters, } from '@/api/moderation'; import { guildMembersQueryKey } from '@/api/guilds'; import { ErrorNotice, LoadingNotice } from '@/components/ui/ErrorNotice'; import { Badge, Button } from '@/components/ui/primitives'; import { formatDateTime } from '@/lib/identity'; import { useApiErrorMessage } from '@/lib/useApiErrorMessage'; import { SettingsSection } from '@/pages/settings/SettingsSection'; /** Пресеты тайм-аута: значения в минутах (0 — снять ограничение). */ const TIMEOUT_PRESETS = [1, 5, 10, 60, 1440] as const; interface GuildBansSectionProps { guildId: string; canBan: boolean; } /** * «Баны»: список забаненных с причиной и автором, снятие бана и установка * тайм-аута участнику (AGENT.md 7.17). Данные обновляются без перезагрузки. */ export function GuildBansSection({ guildId, canBan }: GuildBansSectionProps) { const { t, i18n } = useTranslation(); const locale = i18n.resolvedLanguage ?? 'ru'; const describeError = useApiErrorMessage(); const queryClient = useQueryClient(); const [timeoutMinutes, setTimeoutMinutes] = useState(10); const bans = useQuery({ queryKey: guildBansQueryKey(guildId), queryFn: ({ signal }) => fetchGuildBans(guildId, signal), enabled: canBan, retry: 0, }); const unban = useMutation({ mutationFn: (userId: string) => unbanGuildMember(guildId, userId), onSuccess: (list) => { queryClient.setQueryData(guildBansQueryKey(guildId), list); void queryClient.invalidateQueries({ queryKey: guildMembersQueryKey(guildId) }); }, }); const timeout = useMutation({ mutationFn: ({ userId, minutes }: { userId: string; minutes: number }) => timeoutGuildMember( guildId, userId, minutes === 0 ? '2000-01-01T00:00:00Z' : new Date(Date.now() + minutes * 60_000).toISOString(), ), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: guildMembersQueryKey(guildId) }); }, }); if (!canBan) { return null; } return (
{t('settings.servers.bans.timeoutLabel')} {TIMEOUT_PRESETS.map((minutes) => ( ))}
{bans.isPending ? : null} {bans.isError ? ( void bans.refetch()} /> ) : null} {unban.isError ? (

{describeError(unban.error)}

) : null} {timeout.isError ? (

{describeError(timeout.error)}

) : null} {bans.data !== undefined && bans.data.length === 0 ? (

{t('settings.servers.bans.empty')}

) : null} {bans.data === undefined || bans.data.length === 0 ? null : (
    {bans.data.map((ban) => (
  • {ban.display_name === undefined || ban.display_name === '' ? ban.username : ban.display_name}{' '} @{ban.username} {ban.reason === undefined || ban.reason === '' ? t('settings.servers.bans.noReason') : ban.reason} {' · '} {t('settings.servers.bans.bannedBy', { name: ban.actor_name === undefined || ban.actor_name === '' ? t('settings.servers.bans.unknownActor') : ban.actor_name, date: formatDateTime(ban.created_at, locale), })} {ban.user_id}
  • ))}
)}
); } interface GuildAuditSectionProps { guildId: string; canView: boolean; } /** * «Журнал аудита» с фильтрами: действие, автор и цель (AGENT.md 7.18). * Фильтры уходят в запрос, поэтому сервер отдаёт только нужные записи. */ export function GuildAuditSection({ guildId, canView }: GuildAuditSectionProps) { const { t, i18n } = useTranslation(); const locale = i18n.resolvedLanguage ?? 'ru'; const [filters, setFilters] = useState({ limit: 50 }); const [action, setAction] = useState(''); const [actorId, setActorId] = useState(''); const [targetId, setTargetId] = useState(''); const entries = useQuery({ queryKey: guildAuditQueryKey(guildId, filters), queryFn: ({ signal }) => fetchGuildAudit(guildId, filters, signal), enabled: canView, retry: 0, }); if (!canView) { return null; } const apply = (): void => { setFilters({ limit: 50, action: action.trim(), actorId: actorId.trim(), targetId: targetId.trim(), }); }; const reset = (): void => { setAction(''); setActorId(''); setTargetId(''); setFilters({ limit: 50 }); }; return (
{ event.preventDefault(); apply(); }} >
{entries.isPending ? : null} {entries.isError ? ( void entries.refetch()} /> ) : null} {entries.data !== undefined && entries.data.length === 0 ? (

{t('settings.servers.audit.empty')}

) : null} {entries.data === undefined || entries.data.length === 0 ? null : (
    {entries.data.map((entry) => (
  • {entry.action} {entry.actor_name === undefined ? null : ( · {entry.actor_name} )} {entry.target_id === undefined ? null : ( → {entry.target_id} )} {entry.reason === undefined || entry.reason === '' ? null : ( · {entry.reason} )} {formatDateTime(entry.created_at, locale)}
  • ))}
)}
); }