diff --git a/prisma/migrations/20260413071605_add/migration.sql b/prisma/migrations/20260413071605_add/migration.sql new file mode 100644 index 0000000..1affce6 --- /dev/null +++ b/prisma/migrations/20260413071605_add/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "bug_log" ALTER COLUMN "userId" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 35dbf84..ff103cb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -125,13 +125,13 @@ model BugImage { model BugLog { id String @id @default(uuid()) bugId String - userId String + userId String? status BugStatus description String createdAt DateTime @default(now()) bug Bug @relation(fields: [bugId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("bug_log") } diff --git a/src/app.ts b/src/app.ts index 4c90e03..4e236e6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -260,6 +260,96 @@ export function createApp() { }) }) + .get('/api/bugs', async ({ query }) => { + const page = Number(query.page) || 1 + const limit = Number(query.limit) || 20 + const search = (query.search as string) || '' + const app = query.app as any + const status = query.status as any + + const where: any = {} + if (search) { + where.OR = [ + { description: { contains: search, mode: 'insensitive' } }, + { device: { contains: search, mode: 'insensitive' } }, + { os: { contains: search, mode: 'insensitive' } }, + { affectedVersion: { contains: search, mode: 'insensitive' } }, + ] + } + if (app && app !== 'all') { + where.app = app + } + if (status && status !== 'all') { + where.status = status + } + + const [bugs, total] = await Promise.all([ + prisma.bug.findMany({ + where, + include: { + user: { select: { id: true, name: true, email: true, image: true } }, + images: true, + logs: { + include: { user: { select: { id: true, name: true, image: true } } }, + orderBy: { createdAt: 'desc' }, + }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * limit, + take: limit, + }), + prisma.bug.count({ where }), + ]) + + return { + data: bugs, + totalPages: Math.ceil(total / limit), + totalItems: total, + } + }) + + .post('/api/bugs', async ({ request, set }) => { + const cookie = request.headers.get('cookie') ?? '' + const token = cookie.match(/session=([^;]+)/)?.[1] + let userId: string | undefined + + if (token) { + const session = await prisma.session.findUnique({ where: { token } }) + if (session && session.expiresAt > new Date()) { + userId = session.userId + } + } + + const body = (await request.json()) as any + const bug = await prisma.bug.create({ + data: { + app: body.app, + affectedVersion: body.affectedVersion, + device: body.device, + os: body.os, + status: body.status || 'OPEN', + source: body.source || 'USER', + description: body.description, + stackTrace: body.stackTrace, + userId: userId, + images: body.imageUrl ? { + create: { + imageUrl: body.imageUrl + } + } : undefined, + logs: { + create: { + userId: userId || (await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }))?.id || '', + status: body.status || 'OPEN', + description: 'Bug reported initially.', + }, + }, + }, + }) + + return bug + }) + // ─── Example API ─────────────────────────────────── .get('/api/hello', () => ({ message: 'Hello, world!', diff --git a/src/frontend/components/DashboardLayout.tsx b/src/frontend/components/DashboardLayout.tsx index 1f59101..0cc88c4 100644 --- a/src/frontend/components/DashboardLayout.tsx +++ b/src/frontend/components/DashboardLayout.tsx @@ -29,7 +29,8 @@ import { TbSun, TbMoon, TbUser, - TbHistory + TbHistory, + TbBug } from 'react-icons/tb' interface DashboardLayoutProps { @@ -52,6 +53,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { { label: 'Dashboard', icon: TbDashboard, to: '/dashboard' }, { label: 'Applications', icon: TbApps, to: '/apps' }, { label: 'Log Activity', icon: TbHistory, to: '/logs' }, + { label: 'Bug Reports', icon: TbBug, to: '/bug-reports' }, { label: 'Users', icon: TbUser, to: '/users' }, ] diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts index bb14c69..8c8a833 100644 --- a/src/frontend/config/api.ts +++ b/src/frontend/config/api.ts @@ -31,4 +31,7 @@ export const API_URLS = { getOperators: (page: number, search: string) => `/api/operators?page=${page}&search=${encodeURIComponent(search)}`, getOperatorStats: () => `/api/operators/stats`, + getBugs: (page: number, search: string, app: string, status: string) => + `/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`, + createBug: () => `/api/bugs`, } diff --git a/src/frontend/routes/bug-reports.tsx b/src/frontend/routes/bug-reports.tsx new file mode 100644 index 0000000..eb6a934 --- /dev/null +++ b/src/frontend/routes/bug-reports.tsx @@ -0,0 +1,478 @@ +import { DashboardLayout } from '@/frontend/components/DashboardLayout' +import { API_URLS } from '@/frontend/config/api' +import { + Accordion, + Badge, + Box, + Button, + Code, + Container, + Group, + Image, + Loader, + Modal, + Pagination, + Paper, + Select, + SimpleGrid, + Stack, + Text, + ThemeIcon, + TextInput, + Textarea, + Title, + Timeline, +} from '@mantine/core' +import { useQuery } from '@tanstack/react-query' +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' +import { useDisclosure } from '@mantine/hooks' +import { notifications } from '@mantine/notifications' +import { + TbAlertTriangle, + TbBug, + TbDeviceDesktop, + TbDeviceMobile, + TbFilter, + TbSearch, + TbHistory, + TbPhoto, + TbPlus, + TbCircleCheck, + TbCircleX, +} from 'react-icons/tb' + +export const Route = createFileRoute('/bug-reports')({ + component: ListErrorsPage, +}) + +function ListErrorsPage() { + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const [app, setApp] = useState('all') + const [status, setStatus] = useState('all') + + const { data, isLoading, refetch } = useQuery({ + queryKey: ['bugs', { page, search, app, status }], + queryFn: () => + fetch(API_URLS.getBugs(page, search, app, status)).then((r) => r.json()), + }) + + // Create Bug Modal Logic + const [opened, { open, close }] = useDisclosure(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [createForm, setCreateForm] = useState({ + description: '', + app: 'desa_plus', + status: 'OPEN', + source: 'USER', + affectedVersion: '', + device: '', + os: '', + stackTrace: '', + imageUrl: '', + }) + + 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 res = await fetch(API_URLS.createBug(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createForm), + }) + + if (res.ok) { + notifications.show({ + title: 'Success', + message: 'Bug report has been created.', + color: 'teal', + icon: , + }) + refetch() + close() + setCreateForm({ + description: '', + app: 'desa_plus', + status: 'OPEN', + source: 'USER', + affectedVersion: '', + device: '', + os: '', + stackTrace: '', + imageUrl: '', + }) + } else { + throw new Error('Failed to create bug report') + } + } catch (e) { + notifications.show({ + title: 'Error', + message: 'Something went wrong.', + color: 'red', + icon: , + }) + } finally { + setIsSubmitting(false) + } + } + + const bugs = data?.data || [] + const totalPages = data?.totalPages || 1 + + return ( + + + + + + + Bug Reports + + + Centralized bug tracking and analysis for all applications. + + + + + {/* */} + + + + Report New Bug} + radius="xl" + size="lg" + overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} + > + +