Add notification status to users table in AP
This commit is contained in:
@@ -64,6 +64,28 @@ async def log_admin_action(
|
||||
await db.commit()
|
||||
|
||||
|
||||
def build_admin_user_response(user: User, marathons_count: int) -> AdminUserResponse:
|
||||
"""Build AdminUserResponse from User model."""
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
notify_events=user.notify_events,
|
||||
notify_disputes=user.notify_disputes,
|
||||
notify_moderation=user.notify_moderation,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/users", response_model=list[AdminUserResponse])
|
||||
async def list_users(
|
||||
current_user: CurrentUser,
|
||||
@@ -97,21 +119,7 @@ async def list_users(
|
||||
marathons_count = await db.scalar(
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
response.append(AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
))
|
||||
response.append(build_admin_user_response(user, marathons_count))
|
||||
|
||||
return response
|
||||
|
||||
@@ -130,21 +138,7 @@ async def get_user(user_id: int, current_user: CurrentUser, db: DbSession):
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
)
|
||||
return build_admin_user_response(user, marathons_count)
|
||||
|
||||
|
||||
@router.patch("/users/{user_id}/role", response_model=AdminUserResponse)
|
||||
@@ -184,21 +178,7 @@ async def set_user_role(
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
)
|
||||
return build_admin_user_response(user, marathons_count)
|
||||
|
||||
|
||||
@router.delete("/users/{user_id}", response_model=MessageResponse)
|
||||
@@ -363,21 +343,7 @@ async def ban_user(
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
)
|
||||
return build_admin_user_response(user, marathons_count)
|
||||
|
||||
|
||||
@router.post("/users/{user_id}/unban", response_model=AdminUserResponse)
|
||||
@@ -418,21 +384,7 @@ async def unban_user(
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=None,
|
||||
banned_until=None,
|
||||
ban_reason=None,
|
||||
)
|
||||
return build_admin_user_response(user, marathons_count)
|
||||
|
||||
|
||||
# ============ Reset Password ============
|
||||
@@ -478,21 +430,7 @@ async def reset_user_password(
|
||||
select(func.count()).select_from(Participant).where(Participant.user_id == user.id)
|
||||
)
|
||||
|
||||
return AdminUserResponse(
|
||||
id=user.id,
|
||||
login=user.login,
|
||||
nickname=user.nickname,
|
||||
role=user.role,
|
||||
avatar_url=user.avatar_url,
|
||||
telegram_id=user.telegram_id,
|
||||
telegram_username=user.telegram_username,
|
||||
marathons_count=marathons_count,
|
||||
created_at=user.created_at.isoformat(),
|
||||
is_banned=user.is_banned,
|
||||
banned_at=user.banned_at.isoformat() if user.banned_at else None,
|
||||
banned_until=user.banned_until.isoformat() if user.banned_until else None,
|
||||
ban_reason=user.ban_reason,
|
||||
)
|
||||
return build_admin_user_response(user, marathons_count)
|
||||
|
||||
|
||||
# ============ Force Finish Marathon ============
|
||||
|
||||
@@ -27,6 +27,10 @@ class AdminUserResponse(BaseModel):
|
||||
banned_at: str | None = None
|
||||
banned_until: str | None = None # None = permanent
|
||||
ban_reason: str | None = None
|
||||
# Notification settings
|
||||
notify_events: bool = True
|
||||
notify_disputes: bool = True
|
||||
notify_moderation: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AdminUser, UserRole } from '@/types'
|
||||
import { useToast } from '@/store/toast'
|
||||
import { useConfirm } from '@/store/confirm'
|
||||
import { NeonButton } from '@/components/ui'
|
||||
import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X, KeyRound } from 'lucide-react'
|
||||
import { Search, Ban, UserCheck, Shield, ShieldOff, ChevronLeft, ChevronRight, Users, X, KeyRound, Bell, BellOff } from 'lucide-react'
|
||||
|
||||
export function AdminUsersPage() {
|
||||
const [users, setUsers] = useState<AdminUser[]>([])
|
||||
@@ -195,6 +195,7 @@ export function AdminUsersPage() {
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Роль</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Telegram</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Марафоны</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Уведомления</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Статус</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-400">Действия</th>
|
||||
</tr>
|
||||
@@ -202,13 +203,13 @@ export function AdminUsersPage() {
|
||||
<tbody className="divide-y divide-dark-600">
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-4 py-8 text-center">
|
||||
<td colSpan={9} className="px-4 py-8 text-center">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-2 border-accent-500 border-t-transparent mx-auto" />
|
||||
</td>
|
||||
</tr>
|
||||
) : users.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-4 py-8 text-center text-gray-400">
|
||||
<td colSpan={9} className="px-4 py-8 text-center text-gray-400">
|
||||
Пользователи не найдены
|
||||
</td>
|
||||
</tr>
|
||||
@@ -236,6 +237,30 @@ export function AdminUsersPage() {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400">{user.marathons_count}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.telegram_id ? (
|
||||
<div className="flex items-center gap-1" title={`События: ${user.notify_events ? 'вкл' : 'выкл'}, Споры: ${user.notify_disputes ? 'вкл' : 'выкл'}, Модерация: ${user.notify_moderation ? 'вкл' : 'выкл'}`}>
|
||||
{user.notify_events && user.notify_disputes && user.notify_moderation ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-lg bg-green-500/20 text-green-400 border border-green-500/30">
|
||||
<Bell className="w-3 h-3" />
|
||||
Все
|
||||
</span>
|
||||
) : !user.notify_events && !user.notify_disputes && !user.notify_moderation ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-lg bg-red-500/20 text-red-400 border border-red-500/30">
|
||||
<BellOff className="w-3 h-3" />
|
||||
Откл
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-lg bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
|
||||
<Bell className="w-3 h-3" />
|
||||
Частично
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-600">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.is_banned ? (
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg bg-red-500/20 text-red-400 border border-red-500/30">
|
||||
|
||||
@@ -489,6 +489,10 @@ export interface AdminUser {
|
||||
banned_at: string | null
|
||||
banned_until: string | null // null = permanent ban
|
||||
ban_reason: string | null
|
||||
// Notification settings
|
||||
notify_events: boolean
|
||||
notify_disputes: boolean
|
||||
notify_moderation: boolean
|
||||
}
|
||||
|
||||
export interface AdminMarathon {
|
||||
|
||||
Reference in New Issue
Block a user