import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { CalendarDays, ChevronDown, ChevronLeft, ChevronRight, Search, SlidersHorizontal } from "lucide-react" import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg" import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card" import { Input } from "../../components/ui/input" import { Badge } from "../../components/ui/badge" import { Button } from "../../components/ui/button" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table" import { getDeletionRequests } from "../../api/users.api" import { getRoles } from "../../api/rbac.api" import { cn } from "../../lib/utils" import { TABLE_PAGE_SIZE_OPTIONS } from "../../lib/tablePagination" import type { DeletionRequest, DeletionState, DeletionUserStatus, GetDeletionRequestsParams, } from "../../types/user.types" import type { Role } from "../../types/rbac.types" import { mapDeletionRequestApiItem } from "../../types/user.types" const stateBadge: Record = { PENDING: "bg-amber-100 text-amber-700", DUE: "bg-brand-100 text-brand-600", CANCELLED: "bg-grayScale-200 text-grayScale-600", } const DATE_PLACEHOLDER = "DD-MM-YYYY HH:mm" const STATUS_OPTIONS: DeletionUserStatus[] = ["ACTIVE", "PENDING", "SUSPENDED", "DEACTIVATED"] const STATE_OPTIONS: DeletionState[] = ["PENDING", "DUE", "CANCELLED"] type DateTimeFilterInputProps = { value: string onChange: (value: string) => void placeholder: string } function DateTimeFilterInput({ value, onChange, placeholder }: DateTimeFilterInputProps) { return (
onChange(e.target.value)} onClick={(e) => { // Opens native date-time picker immediately on supported browsers. (e.currentTarget as HTMLInputElement).showPicker?.() }} aria-label={placeholder} className="h-11 rounded-xl border-grayScale-200 bg-white pl-10 pr-3 text-sm shadow-sm transition focus-visible:border-brand-400 focus-visible:ring-brand-200 [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-70 hover:[&::-webkit-calendar-picker-indicator]:opacity-100" />
) } export function DeletionRequestsPage() { const [items, setItems] = useState([]) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [loading, setLoading] = useState(false) const [query, setQuery] = useState("") const [role, setRole] = useState("") const [roleOptions, setRoleOptions] = useState([]) const [rolesLoading, setRolesLoading] = useState(false) const [roleMenuOpen, setRoleMenuOpen] = useState(false) const [roleSearch, setRoleSearch] = useState("") const [statusMenuOpen, setStatusMenuOpen] = useState(false) const [stateMenuOpen, setStateMenuOpen] = useState(false) const [status, setStatus] = useState("") const [state, setState] = useState("") const [requestedAfter, setRequestedAfter] = useState("") const [requestedBefore, setRequestedBefore] = useState("") const [scheduledAfter, setScheduledAfter] = useState("") const [scheduledBefore, setScheduledBefore] = useState("") const roleMenuRef = useRef(null) const statusMenuRef = useRef(null) const stateMenuRef = useRef(null) const toRfc3339 = (value: string) => { if (!value) return undefined const normalized = value.includes("T") ? value : value.replace(" ", "T") const date = new Date(normalized) return Number.isNaN(date.getTime()) ? undefined : date.toISOString() } const fetchData = useCallback(async () => { setLoading(true) try { const params: GetDeletionRequestsParams = { query: query || undefined, role: role || undefined, status: status || undefined, state: state || undefined, requested_after: toRfc3339(requestedAfter), requested_before: toRfc3339(requestedBefore), scheduled_after: toRfc3339(scheduledAfter), scheduled_before: toRfc3339(scheduledBefore), page, page_size: pageSize, } const res = await getDeletionRequests(params) const rows = res.data?.data?.items ?? [] setItems(rows.map(mapDeletionRequestApiItem)) setTotal(res.data?.data?.total ?? 0) } catch (error) { console.error("Failed to fetch deletion requests:", error) setItems([]) setTotal(0) } finally { setLoading(false) } }, [ query, role, status, state, requestedAfter, requestedBefore, scheduledAfter, scheduledBefore, page, pageSize, ]) useEffect(() => { fetchData() }, [fetchData]) useEffect(() => { const fetchRoles = async () => { setRolesLoading(true) try { const pageSize = 50 const firstRes = await getRoles({ page: 1, page_size: pageSize }) const firstBatch = firstRes.data?.data?.roles ?? [] const total = firstRes.data?.data?.total ?? firstBatch.length const totalPages = Math.max(1, Math.ceil(total / pageSize)) let allRoles: Role[] = firstBatch if (totalPages > 1) { const requests: Array> = [] for (let currentPage = 2; currentPage <= totalPages; currentPage += 1) { requests.push(getRoles({ page: currentPage, page_size: pageSize })) } const remaining = await Promise.all(requests) allRoles = [...firstBatch, ...remaining.flatMap((r) => r.data?.data?.roles ?? [])] } const uniqueNames = Array.from( new Set( allRoles .map((r) => r.name?.trim()) .filter((name): name is string => Boolean(name)), ), ).sort((a, b) => a.localeCompare(b)) setRoleOptions(uniqueNames) } catch (error) { console.error("Failed to fetch role options:", error) setRoleOptions([]) } finally { setRolesLoading(false) } } fetchRoles() }, []) useEffect(() => { if (!roleMenuOpen && !statusMenuOpen && !stateMenuOpen) return const handleOutsideClick = (event: MouseEvent) => { const target = event.target as Node if (roleMenuRef.current && !roleMenuRef.current.contains(target)) { setRoleMenuOpen(false) } if (statusMenuRef.current && !statusMenuRef.current.contains(target)) { setStatusMenuOpen(false) } if (stateMenuRef.current && !stateMenuRef.current.contains(target)) { setStateMenuOpen(false) } } document.addEventListener("mousedown", handleOutsideClick) return () => { document.removeEventListener("mousedown", handleOutsideClick) } }, [roleMenuOpen, statusMenuOpen, stateMenuOpen]) const filteredRoleOptions = useMemo(() => { const q = roleSearch.trim().toLowerCase() if (!q) return roleOptions return roleOptions.filter((option) => option.toLowerCase().includes(q)) }, [roleOptions, roleSearch]) const totalPages = useMemo(() => Math.max(1, Math.ceil(total / pageSize)), [total, pageSize]) const safePage = Math.min(page, totalPages) const startEntry = total === 0 ? 0 : (safePage - 1) * pageSize + 1 const endEntry = Math.min(safePage * pageSize, total) const getPageNumbers = () => { const pages: (number | string)[] = [] if (totalPages <= 7) { for (let i = 1; i <= totalPages; i++) pages.push(i) } else { pages.push(1, 2, 3) if (safePage > 4) pages.push("...") if (safePage > 3 && safePage < totalPages - 2) pages.push(safePage) if (safePage < totalPages - 3) pages.push("...") pages.push(totalPages) } return pages } const resetFilters = () => { setQuery("") setRole("") setStatus("") setState("") setRequestedAfter("") setRequestedBefore("") setScheduledAfter("") setScheduledBefore("") setRoleSearch("") setRoleMenuOpen(false) setStatusMenuOpen(false) setStateMenuOpen(false) setPage(1) } return (

Deletion Requests

Review and monitor account deletion requests from users.

Filters
{ setQuery(e.target.value) setPage(1) }} placeholder="Search name, email, phone..." className="h-11 rounded-xl border-grayScale-200 bg-white pl-10 shadow-sm transition focus-visible:border-brand-400 focus-visible:ring-brand-200" />
{roleMenuOpen ? (
setRoleSearch(e.target.value)} placeholder="Search role..." className="h-9 rounded-lg border-grayScale-200 pl-8 text-sm" />
{filteredRoleOptions.map((roleName) => ( ))} {!rolesLoading && filteredRoleOptions.length === 0 ? (

No roles found

) : null}
) : null}
{statusMenuOpen ? (
{STATUS_OPTIONS.map((statusOption) => ( ))}
) : null}
{stateMenuOpen ? (
{STATE_OPTIONS.map((stateOption) => ( ))}
) : null}
{ setRequestedAfter(value) setPage(1) }} placeholder={`Requested after (${DATE_PLACEHOLDER})`} /> { setRequestedBefore(value) setPage(1) }} placeholder={`Requested before (${DATE_PLACEHOLDER})`} /> { setScheduledAfter(value) setPage(1) }} placeholder={`Scheduled after (${DATE_PLACEHOLDER})`} />
{ setScheduledBefore(value) setPage(1) }} placeholder={`Scheduled before (${DATE_PLACEHOLDER})`} />
Requests ({total})
User Role / Status Requested At Scheduled At Deletion State {loading ? (
Loading requests...
) : items.length === 0 ? (

No deletion requests found

Try adjusting your filters

) : ( items.map((item, index) => (

{item.first_name} {item.last_name}

{item.email}

{item.phone_number || "—"}

{item.role || "—"}

{item.status || "—"}

{item.deletion_requested_at || "—"} {item.deletion_scheduled_at || "—"} {item.deletion_state || "—"}
)) )}
Showing {startEntry}-{endEntry} of {total} entries Rows per page
{getPageNumbers().map((n, idx) => typeof n === "string" ? ( ... ) : ( ), )}
) }