Deskripi: - list error report general dan per apps - update status - update feedback No Issues
697 lines
26 KiB
TypeScript
697 lines
26 KiB
TypeScript
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
|
|
import { API_URLS } from '@/frontend/config/api'
|
|
import {
|
|
Accordion,
|
|
Avatar,
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Code,
|
|
Collapse,
|
|
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 [showLogs, setShowLogs] = useState<Record<string, boolean>>({})
|
|
|
|
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: 'desa-plus',
|
|
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<string | null>(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: <TbCircleCheck size={18} />,
|
|
})
|
|
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: <TbCircleX size={18} />,
|
|
})
|
|
} 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: <TbCircleCheck size={18} />,
|
|
})
|
|
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: <TbCircleX size={18} />,
|
|
})
|
|
} 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: <TbCircleCheck size={18} />,
|
|
})
|
|
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: <TbCircleX size={18} />,
|
|
})
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const bugs = data?.data || []
|
|
const totalPages = data?.totalPages || 1
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<Container size="xl" py="lg">
|
|
<Stack gap="xl">
|
|
<Group justify="space-between" align="center">
|
|
<Stack gap={0}>
|
|
<Title order={2} className="gradient-text">
|
|
Error Reports
|
|
</Title>
|
|
<Text size="sm" c="dimmed">
|
|
Centralized error tracking and analysis for all applications.
|
|
</Text>
|
|
</Stack>
|
|
<Group>
|
|
<Button
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
leftSection={<TbPlus size={18} />}
|
|
onClick={open}
|
|
>
|
|
Report Error
|
|
</Button>
|
|
{/* <Button variant="light" color="red" leftSection={<TbBug size={16} />}>
|
|
Generate Report
|
|
</Button> */}
|
|
</Group>
|
|
</Group>
|
|
|
|
<Modal
|
|
opened={updateModalOpened}
|
|
onClose={closeUpdateModal}
|
|
title={<Text fw={700} size="lg">Update Bug Status</Text>}
|
|
radius="xl"
|
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
|
>
|
|
<Stack gap="md">
|
|
<Select
|
|
label="New Status"
|
|
placeholder="Select status"
|
|
required
|
|
data={[
|
|
{ value: 'OPEN', label: 'Open' },
|
|
{ value: 'ON_HOLD', label: 'On Hold' },
|
|
{ value: 'IN_PROGRESS', label: 'In Progress' },
|
|
{ value: 'RESOLVED', label: 'Resolved' },
|
|
{ value: 'RELEASED', label: 'Released' },
|
|
{ value: 'CLOSED', label: 'Closed' },
|
|
]}
|
|
value={updateForm.status}
|
|
onChange={(val) => setUpdateForm({ ...updateForm, status: val || '' })}
|
|
/>
|
|
<Textarea
|
|
label="Update Note (Optional)"
|
|
placeholder="E.g. Fixed in commit xxxxx / Assigned to team"
|
|
minRows={3}
|
|
value={updateForm.description}
|
|
onChange={(e) => setUpdateForm({ ...updateForm, description: e.target.value })}
|
|
/>
|
|
<Button
|
|
fullWidth
|
|
mt="md"
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
loading={isUpdating}
|
|
onClick={handleUpdateStatus}
|
|
>
|
|
Save Changes
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<Modal
|
|
opened={feedbackModalOpened}
|
|
onClose={closeFeedbackModal}
|
|
title={<Text fw={700} size="lg">Developer Feedback</Text>}
|
|
radius="xl"
|
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
|
>
|
|
<Stack gap="md">
|
|
<Textarea
|
|
data-autofocus
|
|
label="Feedback / Note"
|
|
placeholder="Explain the issue, root cause, or resolution..."
|
|
required
|
|
minRows={4}
|
|
value={feedbackForm.feedBack}
|
|
onChange={(e) => setFeedbackForm({ ...feedbackForm, feedBack: e.target.value })}
|
|
/>
|
|
<Button
|
|
fullWidth
|
|
mt="md"
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
loading={isUpdatingFeedback}
|
|
onClick={handleUpdateFeedback}
|
|
>
|
|
Save Feedback
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<Modal
|
|
opened={opened}
|
|
onClose={close}
|
|
title={<Text fw={700} size="lg">Report New Error</Text>}
|
|
radius="xl"
|
|
size="lg"
|
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
|
>
|
|
<Stack gap="md">
|
|
<Textarea
|
|
label="Description"
|
|
placeholder="What happened? Describe the error in detail..."
|
|
required
|
|
minRows={3}
|
|
value={createForm.description}
|
|
onChange={(e) => setCreateForm({ ...createForm, description: e.target.value })}
|
|
/>
|
|
|
|
<SimpleGrid cols={2}>
|
|
<Select
|
|
label="Application"
|
|
data={[
|
|
{ value: 'desa-plus', label: 'Desa+' },
|
|
{ value: 'hipmi', label: 'Hipmi' },
|
|
]}
|
|
value={createForm.app}
|
|
onChange={(val) => setCreateForm({ ...createForm, app: val as any })}
|
|
/>
|
|
<Select
|
|
label="Source"
|
|
data={[
|
|
{ value: 'USER', label: 'User' },
|
|
{ value: 'QC', label: 'QC' },
|
|
{ value: 'SYSTEM', label: 'System' },
|
|
]}
|
|
value={createForm.source}
|
|
onChange={(val) => setCreateForm({ ...createForm, source: val as any })}
|
|
/>
|
|
</SimpleGrid>
|
|
|
|
<SimpleGrid cols={2}>
|
|
<TextInput
|
|
label="Version"
|
|
placeholder="e.g. 2.4.1"
|
|
required
|
|
value={createForm.affectedVersion}
|
|
onChange={(e) => setCreateForm({ ...createForm, affectedVersion: e.target.value })}
|
|
/>
|
|
<Select
|
|
label="Initial Status"
|
|
data={[
|
|
{ value: 'OPEN', label: 'Open' },
|
|
{ value: 'ON_HOLD', label: 'On Hold' },
|
|
{ value: 'IN_PROGRESS', label: 'In Progress' },
|
|
]}
|
|
value={createForm.status}
|
|
onChange={(val) => setCreateForm({ ...createForm, status: val as any })}
|
|
/>
|
|
</SimpleGrid>
|
|
|
|
<SimpleGrid cols={2}>
|
|
<TextInput
|
|
label="Device"
|
|
placeholder="e.g. iPhone 13, Windows 11 PC"
|
|
required
|
|
value={createForm.device}
|
|
onChange={(e) => setCreateForm({ ...createForm, device: e.target.value })}
|
|
/>
|
|
<TextInput
|
|
label="OS"
|
|
placeholder="e.g. iOS 15.4, Windows 11"
|
|
required
|
|
value={createForm.os}
|
|
onChange={(e) => setCreateForm({ ...createForm, os: e.target.value })}
|
|
/>
|
|
</SimpleGrid>
|
|
|
|
<TextInput
|
|
label="Image URL (Optional)"
|
|
placeholder="https://example.com/screenshot.png"
|
|
value={createForm.imageUrl}
|
|
onChange={(e) => setCreateForm({ ...createForm, imageUrl: e.target.value })}
|
|
/>
|
|
|
|
<Textarea
|
|
label="Stack Trace (Optional)"
|
|
placeholder="Paste code or error logs here..."
|
|
style={{ fontFamily: 'monospace' }}
|
|
minRows={2}
|
|
value={createForm.stackTrace}
|
|
onChange={(e) => setCreateForm({ ...createForm, stackTrace: e.target.value })}
|
|
/>
|
|
|
|
<Button
|
|
fullWidth
|
|
mt="md"
|
|
variant="gradient"
|
|
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
|
|
loading={isSubmitting}
|
|
onClick={handleCreateBug}
|
|
>
|
|
Submit Error Report
|
|
</Button>
|
|
</Stack>
|
|
</Modal>
|
|
|
|
<Paper withBorder radius="2xl" className="glass" p="md">
|
|
<SimpleGrid cols={{ base: 1, sm: 4 }} mb="md">
|
|
<TextInput
|
|
placeholder="Search description, device, os..."
|
|
leftSection={<TbSearch size={16} />}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
radius="md"
|
|
/>
|
|
<Select
|
|
placeholder="Application"
|
|
data={[
|
|
{ value: 'all', label: 'All Applications' },
|
|
{ value: 'desa-plus', label: 'Desa+' },
|
|
{ value: 'hipmi', label: 'Hipmi' },
|
|
]}
|
|
value={app}
|
|
onChange={(val) => setApp(val || 'all')}
|
|
radius="md"
|
|
/>
|
|
<Select
|
|
placeholder="Status"
|
|
data={[
|
|
{ value: 'all', label: 'All Status' },
|
|
{ value: 'OPEN', label: 'Open' },
|
|
{ value: 'ON_HOLD', label: 'On Hold' },
|
|
{ value: 'IN_PROGRESS', label: 'In Progress' },
|
|
{ value: 'RESOLVED', label: 'Resolved' },
|
|
{ value: 'RELEASED', label: 'Released' },
|
|
{ value: 'CLOSED', label: 'Closed' },
|
|
]}
|
|
value={status}
|
|
onChange={(val) => setStatus(val || 'all')}
|
|
radius="md"
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="subtle" color="gray" leftSection={<TbFilter size={16} />} onClick={() => {setSearch(''); setApp('all'); setStatus('all')}}>
|
|
Reset
|
|
</Button>
|
|
</Group>
|
|
</SimpleGrid>
|
|
|
|
{isLoading ? (
|
|
<Stack align="center" py="xl">
|
|
<Loader size="lg" type="dots" />
|
|
<Text size="sm" c="dimmed">Loading error reports...</Text>
|
|
</Stack>
|
|
) : bugs.length === 0 ? (
|
|
<Paper p="xl" withBorder style={{ borderStyle: 'dashed', textAlign: 'center' }}>
|
|
<TbBug size={48} color="gray" style={{ marginBottom: 12, opacity: 0.5 }} />
|
|
<Text fw={600}>No error reports found</Text>
|
|
<Text size="sm" c="dimmed">Try adjusting your filters or search terms.</Text>
|
|
</Paper>
|
|
) : (
|
|
<Accordion variant="separated" radius="xl">
|
|
{bugs.map((bug: any) => (
|
|
<Accordion.Item
|
|
key={bug.id}
|
|
value={bug.id}
|
|
style={{
|
|
border: '1px solid var(--mantine-color-default-border)',
|
|
background: 'var(--mantine-color-default)',
|
|
marginBottom: '12px',
|
|
}}
|
|
>
|
|
<Accordion.Control>
|
|
<Group wrap="nowrap">
|
|
<ThemeIcon
|
|
color={
|
|
bug.status === 'OPEN'
|
|
? 'red'
|
|
: bug.status === 'IN_PROGRESS'
|
|
? 'blue'
|
|
: 'teal'
|
|
}
|
|
variant="light"
|
|
size="lg"
|
|
radius="md"
|
|
>
|
|
<TbAlertTriangle size={20} />
|
|
</ThemeIcon>
|
|
<Box style={{ flex: 1 }}>
|
|
<Group justify="space-between">
|
|
<Text size="sm" fw={600} lineClamp={1}>
|
|
{bug.description}
|
|
</Text>
|
|
<Badge
|
|
color={
|
|
bug.status === 'OPEN'
|
|
? 'red'
|
|
: bug.status === 'IN_PROGRESS'
|
|
? 'blue'
|
|
: 'teal'
|
|
}
|
|
variant="dot"
|
|
size="xs"
|
|
>
|
|
{bug.status}
|
|
</Badge>
|
|
</Group>
|
|
<Group gap="md">
|
|
<Text size="xs" c="dimmed">
|
|
{new Date(bug.createdAt).toLocaleString()} • {bug.app?.toUpperCase()} • v{bug.affectedVersion}
|
|
</Text>
|
|
</Group>
|
|
</Box>
|
|
</Group>
|
|
</Accordion.Control>
|
|
<Accordion.Panel>
|
|
<Stack gap="lg" py="xs">
|
|
{/* Device Info */}
|
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVICE METADATA</Text>
|
|
<Group gap="xs">
|
|
{bug.device.toLowerCase().includes('windows') || bug.device.toLowerCase().includes('mac') || bug.device.toLowerCase().includes('pc') ? (
|
|
<TbDeviceDesktop size={14} color="gray" />
|
|
) : (
|
|
<TbDeviceMobile size={14} color="gray" />
|
|
)}
|
|
<Text size="xs" fw={500}>{bug.device} ({bug.os})</Text>
|
|
</Group>
|
|
</Box>
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb={4}>SOURCE</Text>
|
|
<Badge variant="light" color="gray" size="sm">{bug.source}</Badge>
|
|
</Box>
|
|
</SimpleGrid>
|
|
|
|
{/* Feedback & Reporter Info */}
|
|
{(bug.user || bug.feedBack) && (
|
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
|
{bug.user && (
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb={4}>REPORTED BY</Text>
|
|
<Group gap="xs">
|
|
<Avatar src={bug.user.image} radius="xl" size="sm" color="blue">
|
|
{bug.user.name?.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
<Text size="sm">{bug.user.name}</Text>
|
|
</Group>
|
|
</Box>
|
|
)}
|
|
{bug.feedBack && (
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb={4}>DEVELOPER FEEDBACK</Text>
|
|
<Text size="sm" style={{ whiteSpace: 'pre-wrap' }}>{bug.feedBack}</Text>
|
|
</Box>
|
|
)}
|
|
</SimpleGrid>
|
|
)}
|
|
|
|
{/* Stack Trace */}
|
|
{bug.stackTrace && (
|
|
<Box>
|
|
<Text size="xs" fw={700} c="dimmed" mb={4}>STACK TRACE</Text>
|
|
<Code
|
|
block
|
|
color="red"
|
|
style={{
|
|
fontFamily: 'monospace',
|
|
whiteSpace: 'pre-wrap',
|
|
fontSize: '11px',
|
|
border: '1px solid var(--mantine-color-default-border)',
|
|
}}
|
|
>
|
|
{bug.stackTrace}
|
|
</Code>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Images */}
|
|
{bug.images && bug.images.length > 0 && (
|
|
<Box>
|
|
<Group gap="xs" mb={8}>
|
|
<TbPhoto size={16} color="gray" />
|
|
<Text size="xs" fw={700} c="dimmed">ATTACHED IMAGES ({bug.images.length})</Text>
|
|
</Group>
|
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="xs">
|
|
{bug.images.map((img: any) => (
|
|
<Paper key={img.id} withBorder radius="md" style={{ overflow: 'hidden' }}>
|
|
<Image src={img.imageUrl} alt="Error screenshot" height={100} fit="cover" />
|
|
</Paper>
|
|
))}
|
|
</SimpleGrid>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Logs / History */}
|
|
{bug.logs && bug.logs.length > 0 && (
|
|
<Box>
|
|
<Group justify="space-between" mb={showLogs[bug.id] ? 12 : 0}>
|
|
<Group gap="xs">
|
|
<TbHistory size={16} color="gray" />
|
|
<Text size="xs" fw={700} c="dimmed">ACTIVITY LOG ({bug.logs.length})</Text>
|
|
</Group>
|
|
<Button
|
|
variant="subtle"
|
|
size="compact-xs"
|
|
color="gray"
|
|
onClick={() => toggleLogs(bug.id)}
|
|
>
|
|
{showLogs[bug.id] ? 'Hide' : 'Show'}
|
|
</Button>
|
|
</Group>
|
|
<Collapse in={showLogs[bug.id]}>
|
|
<Timeline active={bug.logs.length - 1} bulletSize={24} lineWidth={2} mt="md">
|
|
{bug.logs.map((log: any) => (
|
|
<Timeline.Item
|
|
key={log.id}
|
|
bullet={
|
|
<Badge size="xs" circle color={log.status === 'RESOLVED' ? 'teal' : 'blue'}> </Badge>
|
|
}
|
|
title={<Text size="sm" fw={600}>{log.status}</Text>}
|
|
>
|
|
<Text size="xs" c="dimmed" mb={4}>
|
|
{new Date(log.createdAt).toLocaleString()} by {log.user?.name || 'Unknown'}
|
|
</Text>
|
|
<Text size="sm">{log.description}</Text>
|
|
</Timeline.Item>
|
|
))}
|
|
</Timeline>
|
|
</Collapse>
|
|
</Box>
|
|
)}
|
|
|
|
<Group justify="flex-end" pt="sm">
|
|
<Button variant="light" size="compact-xs" color="blue" onClick={() => {
|
|
setSelectedBugId(bug.id)
|
|
setFeedbackForm({ feedBack: bug.feedBack || '' })
|
|
openFeedbackModal()
|
|
}}>Developer Feedback</Button>
|
|
<Button variant="light" size="compact-xs" color="teal" onClick={() => {
|
|
setSelectedBugId(bug.id)
|
|
setUpdateForm({ status: bug.status, description: '' })
|
|
openUpdateModal()
|
|
}}>Update Status</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Accordion.Panel>
|
|
</Accordion.Item>
|
|
))}
|
|
</Accordion>
|
|
)}
|
|
|
|
{totalPages > 1 && (
|
|
<Group justify="center" mt="xl">
|
|
<Pagination total={totalPages} value={page} onChange={setPage} radius="xl" />
|
|
</Group>
|
|
)}
|
|
</Paper>
|
|
</Stack>
|
|
</Container>
|
|
</DashboardLayout>
|
|
)
|
|
}
|