import { Metadata } from 'next'; import Link from 'next/link'; import { FeedbackEntryType, FeedbackStatus } from '@prisma/client'; import { format } from 'date-fns'; import { redirect } from 'next/navigation'; import { auth } from '@/lib/auth'; import { db } from '@/lib/db'; import { DeleteFeedbackButton } from '@/components/admin/delete-feedback-button'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; type SortBy = | 'submittedAt' | 'type' | 'status' | 'rating' | 'user' | 'allowShowcase' | 'showOnLanding'; type SortDirection = 'asc' | 'desc'; type TypeFilter = 'ALL' | FeedbackEntryType; type StatusFilter = 'ALL' | FeedbackStatus; type AdminFeedbackEntry = { id: string; type: FeedbackEntryType; category: string | null; title: string; message: string; screenshotUrl: string | null; screenshots: Array<{ id: string; url: string }>; rating: number | null; status: FeedbackStatus; allowShowcase: boolean; showOnLanding: boolean; createdAt: Date; user: { id: string; name: string | null; email: string | null }; }; function parseSortBy(value: string | undefined): SortBy { const accepted: SortBy[] = [ 'submittedAt', 'type', 'status', 'rating', 'user', 'allowShowcase', 'showOnLanding', ]; return accepted.includes(value as SortBy) ? (value as SortBy) : 'submittedAt'; } function parseSortDirection(value: string | undefined): SortDirection { return value === 'asc' || value === 'desc' ? value : 'desc'; } function parseTypeFilter(value: string | undefined): TypeFilter { if (value === FeedbackEntryType.FEEDBACK || value === FeedbackEntryType.REVIEW) return value; return 'ALL'; } function parseStatusFilter(value: string | undefined): StatusFilter { const accepted: FeedbackStatus[] = ['NEW', 'IN_REVIEW', 'APPROVED', 'REJECTED', 'RESOLVED']; if (accepted.includes(value as FeedbackStatus)) return value as FeedbackStatus; return 'ALL'; } function getSortIndicator( column: SortBy, activeSortBy: SortBy, activeSortDirection: SortDirection ): string { if (column !== activeSortBy) return '↕'; return activeSortDirection === 'asc' ? '↑' : '↓'; } function getOrderBy(sortBy: SortBy, sortDirection: SortDirection): unknown { const createdAtTieBreaker = { createdAt: 'desc' as const }; if (sortBy === 'submittedAt') { return [{ createdAt: sortDirection }]; } if (sortBy === 'type') { return [{ type: sortDirection }, createdAtTieBreaker]; } if (sortBy === 'status') { return [{ status: sortDirection }, createdAtTieBreaker]; } if (sortBy === 'rating') { return [{ rating: sortDirection }, createdAtTieBreaker]; } if (sortBy === 'allowShowcase') { return [{ allowShowcase: sortDirection }, createdAtTieBreaker]; } if (sortBy === 'showOnLanding') { return [{ showOnLanding: sortDirection }, createdAtTieBreaker]; } return [ { user: { name: sortDirection } }, { user: { email: sortDirection } }, createdAtTieBreaker, ]; } export const metadata: Metadata = { title: 'Feedback | Admin', }; export default async function AdminFeedbackPage({ searchParams, }: { searchParams: Promise<{ page?: string; sortBy?: string; sortDirection?: string; type?: string; status?: string; }>; }) { const session = await auth(); if (!session?.user?.isAdmin) { redirect('/'); } const params = await searchParams; const rawPage = Number(params.page); const requestedPage = Number.isFinite(rawPage) ? Math.min(Math.max(1, rawPage), 500) : 1; const pageSize = 20; const sortBy = parseSortBy(params.sortBy); const sortDirection = parseSortDirection(params.sortDirection); const typeFilter = parseTypeFilter(params.type); const statusFilter = parseStatusFilter(params.status); const where = { ...(typeFilter !== 'ALL' ? { type: typeFilter } : {}), ...(statusFilter !== 'ALL' ? { status: statusFilter } : {}), }; const orderBy = getOrderBy(sortBy, sortDirection); const userFeedbackDelegate = ( db as unknown as { userFeedback?: { count: (args?: unknown) => Promise; findMany: (args?: unknown) => Promise; }; } ).userFeedback; let totalEntries = 0; let page = requestedPage; let entries: AdminFeedbackEntry[] = []; if (userFeedbackDelegate) { try { totalEntries = await userFeedbackDelegate.count({ where }); const totalPages = Math.max(1, Math.ceil(totalEntries / pageSize)); page = Math.min(requestedPage, totalPages); const skip = (page - 1) * pageSize; entries = await userFeedbackDelegate.findMany({ where, skip, take: pageSize, include: { user: { select: { id: true, name: true, email: true, }, }, screenshots: { select: { id: true, url: true, }, orderBy: { createdAt: 'asc' }, }, }, orderBy, }); } catch (error) { const message = error instanceof Error ? error.message : ''; if (message.includes('Unknown field `screenshots`')) { try { totalEntries = await userFeedbackDelegate.count({ where }); const totalPages = Math.max(1, Math.ceil(totalEntries / pageSize)); page = Math.min(requestedPage, totalPages); const skip = (page - 1) * pageSize; const fallbackEntries = (await userFeedbackDelegate.findMany({ where, skip, take: pageSize, include: { user: { select: { id: true, name: true, email: true, }, }, }, orderBy, })) as Array< Omit & { screenshots?: Array<{ id: string; url: string }>; } >; entries = fallbackEntries.map((entry) => ({ id: entry.id, type: entry.type, category: entry.category, title: entry.title, message: entry.message, screenshotUrl: entry.screenshotUrl, screenshots: Array.isArray(entry.screenshots) ? entry.screenshots : [], rating: entry.rating, status: entry.status, allowShowcase: entry.allowShowcase, showOnLanding: entry.showOnLanding, createdAt: entry.createdAt, user: entry.user, })); } catch (fallbackError) { console.error('Failed to fetch feedback entries (fallback):', fallbackError); } } else { console.error('Failed to fetch feedback entries:', error); } } } const totalPages = Math.max(1, Math.ceil(totalEntries / pageSize)); const paginatedEntries = entries; const buildPageHref = ( targetPage: number, targetSortBy: SortBy = sortBy, targetSortDirection: SortDirection = sortDirection, targetType: TypeFilter = typeFilter, targetStatus: StatusFilter = statusFilter ): string => { const next = new URLSearchParams({ page: String(targetPage), sortBy: targetSortBy, sortDirection: targetSortDirection, type: targetType, status: targetStatus, }); return `/admin/feedback?${next.toString()}`; }; const buildSortHref = (column: SortBy): string => { const nextDirection: SortDirection = column === sortBy ? sortDirection === 'asc' ? 'desc' : 'asc' : column === 'user' ? 'asc' : 'desc'; return buildPageHref(1, column, nextDirection); }; const buildFilterHref = (targetType: TypeFilter, targetStatus: StatusFilter): string => buildPageHref(1, sortBy, sortDirection, targetType, targetStatus); return (

Feedback & Reviews

{(['NEW', 'IN_REVIEW', 'APPROVED', 'REJECTED', 'RESOLVED'] as const).map((status) => ( ))}
Submissions {totalEntries} submission(s) found.
Submitted {getSortIndicator('submittedAt', sortBy, sortDirection)} User {getSortIndicator('user', sortBy, sortDirection)} Type {getSortIndicator('type', sortBy, sortDirection)} Title Message Screenshot Rating {getSortIndicator('rating', sortBy, sortDirection)} Status {getSortIndicator('status', sortBy, sortDirection)} Consent {getSortIndicator('allowShowcase', sortBy, sortDirection)} Landing {getSortIndicator('showOnLanding', sortBy, sortDirection)} Actions {paginatedEntries.length === 0 ? ( No submissions found. ) : ( paginatedEntries.map((entry) => ( {format(new Date(entry.createdAt), 'MMM dd, yyyy HH:mm')}
{entry.user.name || 'Anonymous'} {entry.user.email}
{entry.type === 'FEEDBACK' ? 'Feedback' : 'Review'} {entry.category && ( {entry.category} )}
{entry.title} {entry.message} {entry.screenshots.length > 0 || entry.screenshotUrl ? ( {entry.screenshots.length || (entry.screenshotUrl ? 1 : 0)} image {(entry.screenshots.length || (entry.screenshotUrl ? 1 : 0)) > 1 ? 's' : ''} ) : ( '-' )} {entry.rating ?? '-'} {entry.status} {entry.allowShowcase ? 'Yes' : 'No'} {entry.showOnLanding ? 'Yes' : 'No'}
)) )}
{totalPages > 1 && (
Page {page} of {totalPages}
)}
); }