diff --git a/src/app.ts b/src/app.ts index f4f93e4..643281c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -89,7 +89,7 @@ export function createApp() { access_type: 'offline', prompt: 'consent', }) - set.status = 302; set.headers['location'] =`https://accounts.google.com/o/oauth2/v2/auth?${params}` + set.status = 302; set.headers['location'] = `https://accounts.google.com/o/oauth2/v2/auth?${params}` }) .get('/api/auth/callback/google', async ({ request, set }) => { @@ -98,7 +98,7 @@ export function createApp() { const origin = url.origin if (!code) { - set.status = 302; set.headers['location'] ='/login?error=google_failed' + set.status = 302; set.headers['location'] = '/login?error=google_failed' return } @@ -116,7 +116,7 @@ export function createApp() { }) if (!tokenRes.ok) { - set.status = 302; set.headers['location'] ='/login?error=google_failed' + set.status = 302; set.headers['location'] = '/login?error=google_failed' return } @@ -128,7 +128,7 @@ export function createApp() { }) if (!userInfoRes.ok) { - set.status = 302; set.headers['location'] ='/login?error=google_failed' + set.status = 302; set.headers['location'] = '/login?error=google_failed' return } @@ -382,6 +382,73 @@ export function createApp() { return bug }) + .patch('/api/bugs/:id/feedback', async ({ params: { id }, request }) => { + 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 { feedBack: string } + const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }) + const actingUserId = userId || defaultAdmin?.id || undefined + + const bug = await prisma.bug.update({ + where: { id }, + data: { + feedBack: body.feedBack, + }, + }) + + if (actingUserId) { + await createSystemLog(actingUserId, 'UPDATE', `Updated bug report feedback - ${id}`) + } + + return bug + }) + + .patch('/api/bugs/:id/status', async ({ params: { id }, request }) => { + 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 { status: string; description?: string } + const defaultAdmin = await prisma.user.findFirst({ where: { role: 'SUPER_ADMIN' } }) + const actingUserId = userId || defaultAdmin?.id || undefined + + const bug = await prisma.bug.update({ + where: { id }, + data: { + status: body.status as any, + logs: { + create: { + userId: actingUserId, + status: body.status as any, + description: body.description || `Status updated to ${body.status}`, + }, + }, + }, + }) + + if (actingUserId) { + await createSystemLog(actingUserId, 'UPDATE', `Updated bug report status to ${body.status}-${id}`) + } + + 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 0cc88c4..5ec63f3 100644 --- a/src/frontend/components/DashboardLayout.tsx +++ b/src/frontend/components/DashboardLayout.tsx @@ -12,25 +12,26 @@ import { Select, Stack, Text, - ThemeIcon + ThemeIcon, + useComputedColorScheme, + useMantineColorScheme } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' -import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core' import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router' import { + TbAlertTriangle, TbApps, TbArrowLeft, TbChevronRight, TbDashboard, TbDeviceMobile, - TbLogout, - TbSettings, - TbUserCircle, - TbSun, - TbMoon, - TbUser, TbHistory, - TbBug + TbLogout, + TbMoon, + TbSettings, + TbSun, + TbUser, + TbUserCircle } from 'react-icons/tb' interface DashboardLayoutProps { @@ -53,7 +54,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: 'Error Reports', icon: TbAlertTriangle, to: '/bug-reports' }, { label: 'Users', icon: TbUser, to: '/users' }, ] diff --git a/src/frontend/config/api.ts b/src/frontend/config/api.ts index 9472c17..dde9bee 100644 --- a/src/frontend/config/api.ts +++ b/src/frontend/config/api.ts @@ -34,5 +34,7 @@ export const API_URLS = { getBugs: (page: number, search: string, app: string, status: string) => `/api/bugs?page=${page}&search=${encodeURIComponent(search)}&app=${app}&status=${status}`, createBug: () => `/api/bugs`, + updateBugStatus: (id: string) => `/api/bugs/${id}/status`, + updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`, createLog: () => `/api/logs`, } diff --git a/src/frontend/routes/apps.$appId.errors.tsx b/src/frontend/routes/apps.$appId.errors.tsx index 071ef1b..4fabc65 100644 --- a/src/frontend/routes/apps.$appId.errors.tsx +++ b/src/frontend/routes/apps.$appId.errors.tsx @@ -1,78 +1,238 @@ import { - Badge, - Container, - Group, - Stack, - Text, - Title, - Paper, Accordion, - ThemeIcon, - TextInput, - Select, - Code, + Avatar, + Badge, Box, Button, + Code, + Collapse, + Group, + Image, + Loader, + Modal, + Pagination, + Paper, + Select, SimpleGrid, + Stack, + Text, + Textarea, + TextInput, + ThemeIcon, + Timeline, + Title } from '@mantine/core' +import { useDisclosure } from '@mantine/hooks' +import { notifications } from '@mantine/notifications' +import { useQuery } from '@tanstack/react-query' import { createFileRoute, useParams } from '@tanstack/react-router' -import { - TbAlertTriangle, - TbBug, - TbDeviceDesktop, - TbDeviceMobile, - TbSearch, - TbFilter, +import { useState } from 'react' +import { + TbAlertTriangle, + TbBug, TbCircleCheck, - TbUserCheck + TbCircleX, + TbDeviceDesktop, + TbDeviceMobile, + TbFilter, + TbHistory, + TbPhoto, + TbPlus, + TbSearch } from 'react-icons/tb' +import { API_URLS } from '../config/api' export const Route = createFileRoute('/apps/$appId/errors')({ component: AppErrorsPage, }) -const mockErrors = [ - { - id: 1, - title: 'NullPointerException: village_id is null', - message: 'Occurred during background sync with central server.', - version: '2.4.1', - device: 'PC Admin (Windows 10)', - time: '2 mins ago', - severity: 'critical', - users: 24, - frequency: 145, - stackTrace: 'at com.desa.sync.VillageManager.sync(VillageManager.java:45)\nat com.desa.sync.SyncService.onHandleIntent(SyncService.java:120)\nat android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:78)' - }, - { - id: 2, - title: 'SocketTimeoutException: Connection reset by peer', - message: 'Failed to upload document: surat_kematian_01.pdf', - version: '2.4.0', - device: 'Android Tablet (Samsung Tab A8)', - time: '15 mins ago', - severity: 'high', - users: 5, - frequency: 12, - stackTrace: 'java.net.SocketTimeoutException: timeout\nat okio.Okio$4.newTimeoutException(Okio.java:232)\nat okio.AsyncTimeout.exit(AsyncTimeout.java:285)' - }, - { - id: 3, - title: 'SQLiteException: no such column: village_id', - message: 'Failed to query local village profile database.', - version: '2.4.1', - device: 'PC Admin (Windows 7)', - time: '1 hour ago', - severity: 'medium', - users: 2, - frequency: 4, - stackTrace: 'java.io.IOException: No space left on device\nat java.io.FileOutputStream.writeBytes(Native Method)' - }, -] function AppErrorsPage() { const { appId } = useParams({ from: '/apps/$appId/errors' }) + const [page, setPage] = useState(1) + const [search, setSearch] = useState('') + const [app, setApp] = useState(appId) + const [status, setStatus] = useState('all') + + + const [showLogs, setShowLogs] = useState>({}) + + const toggleLogs = (bugId: string) => { + setShowLogs((prev) => ({ ...prev, [bugId]: !prev[bugId] })) + } + + 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: appId, + status: 'OPEN', + source: 'USER', + affectedVersion: '', + device: '', + os: '', + stackTrace: '', + imageUrl: '', + }) + + // Update Status Modal Logic + const [updateModalOpened, { open: openUpdateModal, close: closeUpdateModal }] = useDisclosure(false) + const [isUpdating, setIsUpdating] = useState(false) + const [selectedBugId, setSelectedBugId] = useState(null) + const [updateForm, setUpdateForm] = useState({ + status: '', + description: '', + }) + + // Feedback Modal Logic + 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('Failed to update feedback') + } + } catch (e) { + 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('Failed to update status') + } + } catch (e) { + 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 res = await fetch(API_URLS.createBug(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createForm), + }) + + if (res.ok) { + await fetch(API_URLS.createLog(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'CREATE', message: `Report error baru ditambahkan: ${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() + setCreateForm({ + description: '', + app: 'desa_plus', + status: 'OPEN', + source: 'USER', + affectedVersion: '', + device: '', + os: '', + stackTrace: '', + imageUrl: '', + }) + } else { + throw new Error('Failed to create error 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 ( @@ -80,93 +240,434 @@ function AppErrorsPage() { Error Reporting Center Advanced analysis of health issues and crashes for {appId}. - - - + Update Bug Status} + radius="xl" + overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} + > + +