import { DashboardLayout } from '@/frontend/components/DashboardLayout' import { SummaryCard } from '@/frontend/components/SummaryCard' import { API_URLS } from '@/frontend/config/api' import { AreaChart, BarChart } from '@mantine/charts' import { Accordion, Avatar, Badge, Box, Button, Code, Collapse, Container, FileInput, Group, Image, Loader, Modal, Pagination, Paper, Select, SimpleGrid, Stack, Text, TextInput, Textarea, ThemeIcon, Timeline, Title, Tooltip, } from '@mantine/core' import { useDebouncedValue, useDisclosure } from '@mantine/hooks' import { DatePickerInput, type DatesRangeValue } from '@mantine/dates' import { notifications } from '@mantine/notifications' import { useQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' import dayjs from 'dayjs' import { useEffect, useState } from 'react' import { TbAlertTriangle, TbBug, TbChartBar, TbCircleCheck, TbCircleX, TbClock, TbDeviceDesktop, TbDeviceMobile, TbFilter, TbHistory, TbPhoto, TbPlus, TbSearch, TbTrendingUp, } from 'react-icons/tb' import useSWR from 'swr' export const Route = createFileRoute('/bug-reports')({ component: ListErrorsPage, }) const STATUS_COLOR: Record = { OPEN: 'red', IN_PROGRESS: 'blue', ON_HOLD: 'orange', RESOLVED: 'teal', RELEASED: 'green', CLOSED: 'gray', } const STATUS_LABEL: Record = { OPEN: 'Open', ON_HOLD: 'On Hold', IN_PROGRESS: 'In Progress', RESOLVED: 'Resolved', RELEASED: 'Released', CLOSED: 'Closed', } function ListErrorsPage() { const [page, setPage] = useState(1) const [search, setSearch] = useState('') const [searchQuery, setSearchQuery] = useState('') const [app, setApp] = useState('all') const [status, setStatus] = useState('all') const [source, setSource] = useState('all') const [dateRange, setDateRange] = useState([null, null]) const [bugRange, setBugRange] = useState<7 | 30 | 90>(7) const [debouncedSearch] = useDebouncedValue(search, 400) useEffect(() => { if (debouncedSearch.length >= 3 || debouncedSearch.length === 0) { setSearchQuery(debouncedSearch) setPage(1) } }, [debouncedSearch]) useEffect(() => { setPage(1) }, [app, status, source, dateRange]) const [showLogs, setShowLogs] = useState>({}) const [showStackTrace, setShowStackTrace] = useState>({}) const dateFrom = dateRange[0] ? dayjs(dateRange[0]).format('YYYY-MM-DD') : undefined const dateTo = dateRange[1] ? dayjs(dateRange[1]).format('YYYY-MM-DD') : undefined const toggleLogs = (bugId: string) => setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] })) const toggleStackTrace = (bugId: string) => setShowStackTrace((prev) => ({ ...prev, [bugId]: !prev[bugId] })) const { data, isLoading, refetch } = useQuery({ queryKey: ['bugs', { page, searchQuery, app, status, source, dateFrom, dateTo }], queryFn: () => fetch(API_URLS.getBugs(page, searchQuery, app, status, source, dateFrom, dateTo)).then((r) => r.json()), }) const { data: bugStats } = useSWR(API_URLS.getBugStats(bugRange), (url: string) => fetch(url).then((r) => r.json())) const { data: appsList } = useQuery({ queryKey: ['apps-list'], queryFn: () => fetch('/api/apps').then((r) => r.json()), }) const [previewImage, setPreviewImage] = useState(null) const [opened, { open, close }] = useDisclosure(false) const [isSubmitting, setIsSubmitting] = useState(false) const [imageFiles, setImageFiles] = useState([]) const [createForm, setCreateForm] = useState({ description: '', app: 'desa-plus', source: 'USER', affectedVersion: '', device: '', os: '', stackTrace: '', }) const [updateModalOpened, { open: openUpdateModal, close: closeUpdateModal }] = useDisclosure(false) const [isUpdating, setIsUpdating] = useState(false) const [selectedBugId, setSelectedBugId] = useState(null) const [updateForm, setUpdateForm] = useState({ status: '', description: '' }) const [feedbackModalOpened, { open: openFeedbackModal, close: closeFeedbackModal }] = useDisclosure(false) const [isUpdatingFeedback, setIsUpdatingFeedback] = useState(false) const [feedbackForm, setFeedbackForm] = useState({ feedBack: '' }) const handleUpdateFeedback = async () => { if (!selectedBugId || !feedbackForm.feedBack) return setIsUpdatingFeedback(true) try { const res = await fetch(API_URLS.updateBugFeedback(selectedBugId), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feedbackForm), }) if (res.ok) { notifications.show({ title: 'Success', message: 'Feedback has been updated.', color: 'teal', icon: }) refetch() closeFeedbackModal() setFeedbackForm({ feedBack: '' }) } else { throw new Error() } } catch { notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: }) } finally { setIsUpdatingFeedback(false) } } const handleUpdateStatus = async () => { if (!selectedBugId || !updateForm.status) return setIsUpdating(true) try { const res = await fetch(API_URLS.updateBugStatus(selectedBugId), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateForm), }) if (res.ok) { notifications.show({ title: 'Success', message: 'Status has been updated.', color: 'teal', icon: }) refetch() closeUpdateModal() setUpdateForm({ status: '', description: '' }) } else { throw new Error() } } catch { notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: }) } finally { setIsUpdating(false) } } const handleCreateBug = async () => { if (!createForm.description || !createForm.affectedVersion || !createForm.device || !createForm.os) { notifications.show({ title: 'Validation Error', message: 'Please fill in all required fields.', color: 'red' }) return } setIsSubmitting(true) try { const imageUrls: string[] = [] for (const file of imageFiles) { const formData = new FormData() formData.append('file', file) const uploadRes = await fetch(API_URLS.uploadImage(), { method: 'POST', body: formData }) if (!uploadRes.ok) throw new Error('Failed to upload image') const { url } = await uploadRes.json() imageUrls.push(url) } const res = await fetch(API_URLS.createBug(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...createForm, imageUrls: imageUrls.length ? imageUrls : undefined }), }) if (res.ok) { await fetch(API_URLS.createLog(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'CREATE', message: `New error report added: ${createForm.description.substring(0, 50)}${createForm.description.length > 50 ? '...' : ''}` }), }).catch(console.error) notifications.show({ title: 'Success', message: 'Error report has been created.', color: 'teal', icon: }) refetch() close() setImageFiles([]) setCreateForm({ description: '', app: 'desa-plus', source: 'USER', affectedVersion: '', device: '', os: '', stackTrace: '' }) } else { throw new Error() } } catch { notifications.show({ title: 'Error', message: 'Something went wrong.', color: 'red', icon: }) } finally { setIsSubmitting(false) } } const bugs = data?.data || [] const totalPages = data?.totalPages || 1 return ( Error Reports Centralized error tracking and analysis for all applications. {/* Bug Statistics Section */} {bugStats && ( 0} /> )} {bugStats && ( Bugs per Application ({ ...item, appId: item.appId.split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' '), }))} dataKey="appId" series={[{ name: 'count', color: 'blue.6' }]} withTooltip tickLine="none" gridAxis="x" barProps={{ radius: [8, 8, 0, 0], fill: 'url(#bugBarGradient)', }} xAxisProps={{ tick: { fontSize: 12, fill: '#909296' }, }} tooltipProps={{ content: ({ active, payload }: any) => { if (!active || !payload?.length) return null return (
{payload[0]?.payload?.appId}
Bugs: {payload[0]?.value}
) }, }} >
Bug Trend Last {bugRange} days {([7, 30, 90] as const).map((r) => ( ))} { if (!active || !payload?.length) return null return (
{payload[0]?.payload?.date}
Bugs: {payload[0]?.value}
) }, }} styles={{ root: { '.recharts-area-curve': { strokeWidth: 2.5, filter: 'drop-shadow(0 3px 6px rgba(124, 58, 237, 0.3))', }, }, }} />
)} {/* Image Preview Modal */} setPreviewImage(null)} size="xl" radius="md" padding={0} withCloseButton={false} overlayProps={{ backgroundOpacity: 0.75, blur: 6 }} styles={{ content: { background: 'transparent', boxShadow: 'none' } }} onClick={() => setPreviewImage(null)} > {previewImage && ( Preview )} Update Bug Status} radius="md" overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} >