import { useCallback, useEffect, useState } from "react"; import { Search, ChevronDown, ChevronLeft, ChevronRight, AlertCircle, Eye, RefreshCw, Clock, User, Trash2, X, Info, Bug, Video, BookOpen, HelpCircle, Loader2, CheckCircle2, XCircle, ArrowUpCircle, MessageCircle, } from "lucide-react"; import { Button } from "../../components/ui/button"; import { Input } from "../../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/ui/table"; import { Badge } from "../../components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "../../components/ui/dialog"; import { cn } from "../../lib/utils"; import { SpinnerIcon } from "../../components/ui/spinner-icon"; import { getIssues, getIssueById, updateIssueStatus, deleteIssue, createIssue, } from "../../api/issues.api"; import type { Issue, IssueFilters } from "../../types/issue.types"; // ── Status configuration ─────────────────────────────────────────── const STATUSES = ["pending", "in_progress", "resolved", "closed"] as const; const ISSUE_TYPES = ["bug", "video", "course", "account", "payment", "other"] as const; function getStatusConfig(status: string): { label: string; classes: string; icon: typeof CheckCircle2; } { switch (status) { case "pending": return { label: "Pending", classes: "bg-amber-50 text-amber-700 border-amber-200", icon: Clock, }; case "in_progress": return { label: "In Progress", classes: "bg-blue-50 text-blue-700 border-blue-200", icon: Loader2, }; case "resolved": return { label: "Resolved", classes: "bg-emerald-50 text-emerald-700 border-emerald-200", icon: CheckCircle2, }; case "closed": return { label: "Closed", classes: "bg-grayScale-100 text-grayScale-500 border-grayScale-200", icon: XCircle, }; default: return { label: status, classes: "bg-grayScale-100 text-grayScale-600 border-grayScale-200", icon: HelpCircle, }; } } function getIssueTypeConfig(type: string | null | undefined): { label: string; classes: string; icon: typeof Bug; } { const t = String(type ?? "").trim(); switch (t) { case "bug": return { label: "Bug", classes: "bg-red-50 text-red-700 border-red-200", icon: Bug, }; case "video": return { label: "Video", classes: "bg-violet-50 text-violet-700 border-violet-200", icon: Video, }; case "course": return { label: "Course", classes: "bg-brand-500 text-white border-brand-500", icon: BookOpen, }; case "account": return { label: "Account", classes: "bg-sky-50 text-sky-700 border-sky-200", icon: User, }; case "payment": return { label: "Payment", classes: "bg-emerald-50 text-emerald-700 border-emerald-200", icon: ArrowUpCircle, }; default: return { label: t ? t.charAt(0).toUpperCase() + t.slice(1) : "Other", classes: "bg-grayScale-100 text-grayScale-600 border-grayScale-200", icon: HelpCircle, }; } } function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); } function formatDateTime(dateStr: string): string { return new Date(dateStr).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } function getRelativeTime(dateStr: string): string { const now = new Date(); const date = new Date(dateStr); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return "Just now"; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return formatDate(dateStr); } function formatRoleLabel(role: string | null | undefined): string { const r = String(role ?? "").trim(); if (!r) return "—"; return r .split("_") .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) .join(" "); } // ── Main Component ───────────────────────────────────────────────── export function IssuesPage() { const [issues, setIssues] = useState([]); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [typeFilter, setTypeFilter] = useState(""); // Detail dialog const [selectedIssue, setSelectedIssue] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); // Delete confirmation const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [issueToDelete, setIssueToDelete] = useState(null); const [deleteLoading, setDeleteLoading] = useState(false); // Status update const [statusUpdating, setStatusUpdating] = useState(null); // Create issue dialog (admin-created) const [createOpen, setCreateOpen] = useState(false); const [createSubject, setCreateSubject] = useState(""); const [createType, setCreateType] = useState("bug"); const [createDescription, setCreateDescription] = useState(""); const [createDevice, setCreateDevice] = useState(""); const [createBrowser, setCreateBrowser] = useState(""); const [createSubmitting, setCreateSubmitting] = useState(false); const fetchIssues = useCallback(async () => { setLoading(true); try { const filters: IssueFilters = { limit: pageSize, offset: (page - 1) * pageSize, }; const res = await getIssues(filters); const payload = res.data?.data; setIssues(Array.isArray(payload?.issues) ? payload.issues : []); setTotalCount(typeof payload?.total_count === "number" ? payload.total_count : 0); } catch (error) { console.error("Failed to fetch issues:", error); setIssues([]); setTotalCount(0); } finally { setLoading(false); } }, [page, pageSize]); useEffect(() => { fetchIssues(); }, [fetchIssues]); const handleViewDetail = async (issueId: number) => { setDialogOpen(true); setDetailLoading(true); try { const res = await getIssueById(issueId); setSelectedIssue(res.data?.data ?? null); } catch (error) { console.error("Failed to fetch issue detail:", error); } finally { setDetailLoading(false); } }; const handleStatusChange = async (issueId: number, newStatus: string) => { setStatusUpdating(issueId); try { await updateIssueStatus(issueId, newStatus); // Update the issue in the list setIssues((prev) => prev.map((issue) => issue.id === issueId ? { ...issue, status: newStatus } : issue ) ); // Also update the detail dialog if it's showing this issue if (selectedIssue?.id === issueId) { setSelectedIssue((prev) => (prev ? { ...prev, status: newStatus } : prev)); } } catch (error) { console.error("Failed to update issue status:", error); } finally { setStatusUpdating(null); } }; const handleDeleteClick = (issue: Issue) => { setIssueToDelete(issue); setDeleteDialogOpen(true); }; const handleDeleteConfirm = async () => { if (!issueToDelete) return; setDeleteLoading(true); try { await deleteIssue(issueToDelete.id); setDeleteDialogOpen(false); setIssueToDelete(null); // Close detail dialog if the deleted issue was being viewed if (selectedIssue?.id === issueToDelete.id) { setDialogOpen(false); setSelectedIssue(null); } fetchIssues(); } catch (error) { console.error("Failed to delete issue:", error); } finally { setDeleteLoading(false); } }; const hasActiveFilters = statusFilter || typeFilter; const clearFilters = () => { setSearchQuery(""); setStatusFilter(""); setTypeFilter(""); setPage(1); }; // Client-side filtering (status, type, search) const filteredIssues = (Array.isArray(issues) ? issues : []).filter((issue) => { if (statusFilter && issue.status !== statusFilter) return false; if (typeFilter && issue.issue_type !== typeFilter) return false; if (searchQuery) { const q = searchQuery.toLowerCase(); const subject = String(issue.subject ?? "").toLowerCase(); const description = String(issue.description ?? "").toLowerCase(); const issueType = String(issue.issue_type ?? "").toLowerCase(); return subject.includes(q) || description.includes(q) || issueType.includes(q); } return true; }); // Pagination const pageCount = Math.max(1, Math.ceil(totalCount / pageSize)); const safePage = Math.min(page, pageCount); const handlePrev = () => safePage > 1 && setPage(safePage - 1); const handleNext = () => safePage < pageCount && setPage(safePage + 1); const getPageNumbers = () => { const pages: (number | string)[] = []; if (pageCount <= 7) { for (let i = 1; i <= pageCount; i++) pages.push(i); } else { pages.push(1, 2, 3); if (safePage > 4) pages.push("..."); if (safePage > 3 && safePage < pageCount - 2) pages.push(safePage); if (safePage < pageCount - 3) pages.push("..."); pages.push(pageCount); } return pages; }; const startEntry = totalCount === 0 ? 0 : (safePage - 1) * pageSize + 1; const endEntry = Math.min(safePage * pageSize, totalCount); // Stats const pendingCount = issues.filter((i) => i.status === "pending").length; const inProgressCount = issues.filter((i) => i.status === "in_progress").length; const resolvedCount = issues.filter((i) => i.status === "resolved").length; return (
{/* Header */}

Issue Reports

Review and manage user-reported issues across the platform.

{/* Stats cards */}

{totalCount}

Total Issues

{pendingCount}

Pending

{inProgressCount}

In Progress

{resolvedCount}

Resolved

{/* Filters */}
setSearchQuery(e.target.value)} />
{hasActiveFilters && ( )}
{/* Table */}
SUBJECT TYPE STATUS REPORTER CREATED ACTIONS {loading ? (
Loading issues...
) : filteredIssues.length === 0 ? (

No issues found

{hasActiveFilters || searchQuery ? "Try adjusting your filters or search query" : "Reported issues will appear here"}

) : ( filteredIssues.map((issue) => { const typeConfig = getIssueTypeConfig(issue.issue_type); const statusConfig = getStatusConfig(issue.status); const TypeIcon = typeConfig.icon; return (

{issue.subject?.trim() ? issue.subject : "—"}

{issue.description?.trim() ? issue.description : "No description"}

{typeConfig.label}
e.stopPropagation()}>

User #{issue.user_id}

{formatRoleLabel(issue.user_role)}

{formatDate(issue.created_at)}

{getRelativeTime(issue.created_at)}

); }) )}
{/* Pagination */}
Showing {startEntry}–{endEntry} of {totalCount} entries Rows per page
{getPageNumbers().map((n, idx) => typeof n === "string" ? ( ... ) : ( ) )}
{/* Detail Dialog */} Issue Detail Full details for this reported issue. {detailLoading ? (
) : selectedIssue ? (
{/* Subject & badges */}

{selectedIssue.subject}

{getIssueTypeConfig(selectedIssue.issue_type).label} {getStatusConfig(selectedIssue.status).label} ID #{selectedIssue.id}
{/* Description */}

Description

{selectedIssue.description}

{/* Detail grid */}
} label="Reporter" value={`User #${selectedIssue.user_id}`} /> } label="Role" value={formatRoleLabel(selectedIssue.user_role)} /> } label="Created" value={formatDateTime(selectedIssue.created_at)} /> } label="Updated" value={formatDateTime(selectedIssue.updated_at)} />
{/* Status changer */}

Update Status

{STATUSES.map((s) => { const config = getStatusConfig(s); const isActive = selectedIssue.status === s; return ( ); })}
{/* Metadata */} {selectedIssue.metadata && Object.keys(selectedIssue.metadata).length > 0 && (

Metadata

{Object.entries(selectedIssue.metadata).map(([key, value]) => (
{key.replace(/_/g, " ")} {String(value)}
))}
)} {/* Actions */}
) : null}
{/* Create Issue Dialog */} Create admin issue Log an issue directly from the admin panel so it can be tracked and resolved.
setCreateSubject(e.target.value)} />