"use client" import { useEffect, useMemo, useState } from "react" import { MoreHorizontal, UserPlus } from "lucide-react" import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header" import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner" import { useAdminUser, useAdminUsers } from "@/lib/api/hooks/use-admin-queries" import { useDeleteAdminUser, useDisableAdminUser, useInviteAdminUser, useReactivateAdminUser, useSetAdminUserQuota, useSetAdminUserRole, useUpdateAdminUser, } from "@/lib/api/hooks/use-admin-mutations" import type { AdminUser, AdminUserRole } from "@/lib/api/admin-types" import { bytesToGib, formatBytes, gibToBytes } from "@/lib/admin/format-bytes" import { resolveUserRole, USER_ROLE_DESCRIPTIONS, USER_ROLE_FILTER_ALL_ICON, USER_ROLE_ICONS, USER_ROLE_LABELS, } from "@/lib/admin/user-role" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" const ROLE_OPTIONS: AdminUserRole[] = ["admin", "user", "guest", "suspended"] export function UsersSection() { const [q, setQ] = useState("") const [role, setRole] = useState("all") const [page, setPage] = useState(1) const [inviteOpen, setInviteOpen] = useState(false) const [selectedId, setSelectedId] = useState(null) const queryParams = useMemo( () => ({ page, page_size: 25, q: q.trim() || undefined, role: role === "all" ? undefined : role, }), [page, q, role] ) const { data, isFetching, isError, refetch } = useAdminUsers(queryParams) const users = data?.users ?? [] const total = data?.pagination.total ?? 0 const pageSize = data?.pagination.page_size ?? 25 const totalPages = Math.max(1, Math.ceil(total / pageSize)) return ( <> refetch()} />
{ setQ(e.target.value) setPage(1) }} placeholder="E-mail, nom ou ID externe" />
Utilisateur Type Mail Drive ID externe {users.length === 0 ? ( Aucun utilisateur trouvé. ) : ( users.map((user) => ( setSelectedId(user.id)} /> )) )}
{totalPages > 1 ? (
{total.toLocaleString("fr-FR")} utilisateur(s)
) : null} setSelectedId(null)} /> ) } function UserRow({ user, onOpen }: { user: AdminUser; onOpen: () => void }) { const disableUser = useDisableAdminUser() const reactivateUser = useReactivateAdminUser() const deleteUser = useDeleteAdminUser() return (
{user.name || "—"}
{user.email}
{formatBytes(user.storage?.mail_used_bytes ?? 0)} {formatBytes(user.storage?.drive_used_bytes ?? 0)} {user.external_id} e.stopPropagation()}> Détails et quotas {resolveUserRole(user) !== "suspended" ? ( void disableUser.mutateAsync(user.id)} > Suspendre ) : ( void reactivateUser.mutateAsync(user.id)} > Réactiver )} { if (confirm(`Supprimer définitivement ${user.email} ?`)) { void deleteUser.mutateAsync(user.id) } }} > Supprimer
) } function UserRoleLabel({ role, className, }: { role: AdminUserRole | "all" className?: string }) { const Icon = role === "all" ? USER_ROLE_FILTER_ALL_ICON : USER_ROLE_ICONS[role] const label = role === "all" ? "Tous les types" : USER_ROLE_LABELS[role] return ( {label} ) } function RoleBadge({ role }: { role: AdminUserRole }) { const variant = role === "admin" ? "default" : role === "user" ? "secondary" : role === "guest" ? "outline" : "destructive" const Icon = USER_ROLE_ICONS[role] return ( {USER_ROLE_LABELS[role] ?? role} ) } function InviteUserDialog({ open, onOpenChange, }: { open: boolean onOpenChange: (open: boolean) => void }) { const invite = useInviteAdminUser() const [email, setEmail] = useState("") const [name, setName] = useState("") async function submit() { await invite.mutateAsync({ email, name: name || undefined }) setEmail("") setName("") onOpenChange(false) } return ( Inviter un utilisateur
setEmail(e.target.value)} />
setName(e.target.value)} />
) } function UserDetailSheet({ userId, onClose, }: { userId: string | null onClose: () => void }) { const { data: user, isFetching } = useAdminUser(userId) const updateUser = useUpdateAdminUser(userId ?? "") const setRole = useSetAdminUserRole(userId ?? "") const setQuota = useSetAdminUserQuota(userId ?? "") const [name, setName] = useState("") const [email, setEmail] = useState("") const [selectedRole, setSelectedRole] = useState("user") const [mailGib, setMailGib] = useState("5") const [driveGib, setDriveGib] = useState("5") const [photosGib, setPhotosGib] = useState("5") const open = Boolean(userId) useEffect(() => { if (!user) return setName(user.name ?? "") setEmail(user.email ?? "") setSelectedRole(resolveUserRole(user)) if (user.quota) { setMailGib(String(bytesToGib(user.quota.mail.max_storage_bytes).toFixed(1))) setDriveGib(String(bytesToGib(user.quota.drive.max_storage_bytes).toFixed(1))) setPhotosGib(String(bytesToGib(user.quota.photos.max_storage_bytes).toFixed(1))) } }, [user]) async function saveProfile() { if (!userId) return await updateUser.mutateAsync({ name, email }) } async function saveRole() { if (!userId || !user || selectedRole === resolveUserRole(user)) return await setRole.mutateAsync({ role: selectedRole }) } async function saveQuotas() { if (!userId) return await setQuota.mutateAsync({ mail_max_storage_bytes: gibToBytes(Number(mailGib)), drive_max_storage_bytes: gibToBytes(Number(driveGib)), photos_max_storage_bytes: gibToBytes(Number(photosGib)), }) } return ( !v && onClose()}> Détails utilisateur {isFetching || !user ? (

Chargement…

) : (

{USER_ROLE_DESCRIPTIONS[selectedRole]}

setName(e.target.value)} />
setEmail(e.target.value)} />
{user.quota ? (

Quotas stockage

) : null}

ID : {user.id}

)}
) } function coerceBytes(value: unknown): number { const n = typeof value === "number" ? value : Number(value) return Number.isFinite(n) && n >= 0 ? n : 0 } function QuotaField({ label, used, max, gib, onGibChange, count, }: { label: string used: number max: number gib: string onGibChange: (v: string) => void count?: number }) { const maxBytes = max > 0 ? max : gibToBytes(Number(gib) || 0) const pct = maxBytes > 0 ? Math.min(100, Math.round((used / maxBytes) * 100)) : 0 return (
{label} {formatBytes(used)} / {formatBytes(maxBytes)} {count !== undefined ? ` · ${count} messages` : ""}
onGibChange(e.target.value)} /> Go max
) }