amalia/25-mei-26 #26
82
src/app.ts
82
src/app.ts
@@ -1100,6 +1100,88 @@ export function createApp() {
|
||||
},
|
||||
})
|
||||
|
||||
// ─── Bug Statistics API ────────────────────────────
|
||||
.get('/api/bugs/stats', async ({ query }) => {
|
||||
const range = [7, 30, 90].includes(Number(query.range)) ? Number(query.range) : 7
|
||||
const now = new Date()
|
||||
const rangeStart = new Date(now.getTime() - range * 24 * 60 * 60 * 1000)
|
||||
|
||||
const [totalBugs, openBugs, statusGroups, appGroups, sourceGroups, resolvedBugs, trendData] = await Promise.all([
|
||||
prisma.bug.count(),
|
||||
prisma.bug.count({ where: { status: 'OPEN' } }),
|
||||
prisma.bug.groupBy({ by: ['status'], _count: { id: true } }),
|
||||
prisma.bug.groupBy({ by: ['appId'], _count: { id: true } }),
|
||||
prisma.bug.groupBy({ by: ['source'], _count: { id: true } }),
|
||||
prisma.bug.findMany({
|
||||
where: { status: { in: ['RESOLVED', 'CLOSED'] } },
|
||||
select: { createdAt: true, updatedAt: true },
|
||||
}),
|
||||
prisma.bug.findMany({
|
||||
where: { createdAt: { gte: rangeStart } },
|
||||
select: { createdAt: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
}),
|
||||
])
|
||||
|
||||
const byStatus = Object.fromEntries(statusGroups.map((g) => [g.status, g._count.id]))
|
||||
const byApp = appGroups.map((g) => ({ appId: g.appId, count: g._count.id }))
|
||||
const bySource = Object.fromEntries(sourceGroups.map((g) => [g.source, g._count.id]))
|
||||
|
||||
const totalResolutionMs = resolvedBugs.reduce((sum, b) => sum + (b.updatedAt.getTime() - b.createdAt.getTime()), 0)
|
||||
const avgResolutionHours = resolvedBugs.length > 0
|
||||
? Math.round(totalResolutionMs / resolvedBugs.length / (1000 * 60 * 60) * 10) / 10
|
||||
: 0
|
||||
|
||||
const resolvedCount = (byStatus['RESOLVED'] || 0) + (byStatus['CLOSED'] || 0)
|
||||
const resolutionRate = totalBugs > 0 ? Math.round((resolvedCount / totalBugs) * 100) : 0
|
||||
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
const trendMap: Record<string, number> = {}
|
||||
const keyToLabel: Record<string, string> = {}
|
||||
|
||||
for (let i = 0; i < range; i++) {
|
||||
const d = new Date(now)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
const label = `${d.getDate()} ${months[d.getMonth()]}`
|
||||
keyToLabel[key] = label
|
||||
trendMap[key] = 0
|
||||
}
|
||||
for (const b of trendData) {
|
||||
const key = b.createdAt.toISOString().slice(0, 10)
|
||||
if (key in trendMap) trendMap[key]++
|
||||
}
|
||||
const trend: { date: string; count: number }[] = []
|
||||
for (let i = 0; i < range; i++) {
|
||||
const d = new Date(now)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
trend.push({ date: keyToLabel[key] ?? key, count: trendMap[key] ?? 0 })
|
||||
}
|
||||
trend.reverse()
|
||||
|
||||
return {
|
||||
totalBugs,
|
||||
openBugs,
|
||||
byStatus,
|
||||
byApp,
|
||||
bySource,
|
||||
avgResolutionHours,
|
||||
resolutionRate,
|
||||
trend,
|
||||
range,
|
||||
}
|
||||
}, {
|
||||
query: t.Object({
|
||||
range: t.Optional(t.String({ description: 'Rentang hari: 7, 30, atau 90 (default: 30)' })),
|
||||
}),
|
||||
detail: {
|
||||
summary: 'Bug Statistics',
|
||||
description: 'Statistik bug: total, distribusi status, per app, per source, avg resolution time, dan trend.',
|
||||
tags: ['Bugs'],
|
||||
},
|
||||
})
|
||||
|
||||
// ─── System Status API ─────────────────────────────
|
||||
.get('/api/system/status', async () => {
|
||||
try {
|
||||
|
||||
@@ -7,8 +7,14 @@ export const API_URLS = {
|
||||
`${DESA_PLUS_PROXY}/api/monitoring/info-villages?id=${id}`,
|
||||
gridVillages: (id: string) =>
|
||||
`${DESA_PLUS_PROXY}/api/monitoring/grid-villages?id=${id}`,
|
||||
graphLogVillages: (id: string, time: string) =>
|
||||
`${DESA_PLUS_PROXY}/api/monitoring/graph-log-villages?id=${id}&time=${time}`,
|
||||
graphLogVillages: (id: string, time: string, dateFrom?: string, dateTo?: string) => {
|
||||
const params = new URLSearchParams({ id, time })
|
||||
if (dateFrom) params.set('dateFrom', dateFrom)
|
||||
if (dateTo) params.set('dateTo', dateTo)
|
||||
return `${DESA_PLUS_PROXY}/api/monitoring/graph-log-villages?${params}`
|
||||
},
|
||||
getRecentVillageLogs: (id: string) =>
|
||||
`${DESA_PLUS_PROXY}/api/monitoring/recent-village-logs?id=${id}`,
|
||||
getUsers: (page: number, search: string, isActive?: string, idUserRole?: string, idVillage?: string, orderBy?: string, orderDir?: string) => {
|
||||
const params = new URLSearchParams({ page: String(page), search })
|
||||
if (isActive !== undefined) params.set('isActive', isActive)
|
||||
@@ -59,6 +65,7 @@ export const API_URLS = {
|
||||
return `/api/bugs?${params}`
|
||||
},
|
||||
createBug: () => `/api/bugs`,
|
||||
getBugStats: (range: 7 | 30 | 90 = 30) => `/api/bugs/stats?range=${range}`,
|
||||
uploadImage: () => `/api/upload/image`,
|
||||
updateBugStatus: (id: string) => `/api/bugs/${id}/status`,
|
||||
updateBugFeedback: (id: string) => `/api/bugs/${id}/feedback`,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Switch,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
@@ -19,8 +20,10 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { DatePickerInput, type DatesRangeValue } from '@mantine/dates'
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
TbArrowLeft,
|
||||
@@ -28,6 +31,7 @@ import {
|
||||
TbCalendar,
|
||||
TbCalendarEvent,
|
||||
TbChartBar,
|
||||
TbClock,
|
||||
TbEdit,
|
||||
TbHome2,
|
||||
TbLayoutKanban,
|
||||
@@ -65,11 +69,17 @@ type ChartPeriod = 'daily' | 'monthly' | 'yearly'
|
||||
|
||||
function ActivityChart({ villageId }: { villageId: string }) {
|
||||
const [period, setPeriod] = useState<ChartPeriod>('daily')
|
||||
const [dateRange, setDateRange] = useState<DatesRangeValue>([null, null])
|
||||
|
||||
const { data: response, isLoading } = useSWR(
|
||||
API_URLS.graphLogVillages(villageId, period),
|
||||
fetcher
|
||||
)
|
||||
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 hasCustomRange = !!(dateFrom && dateTo)
|
||||
|
||||
const apiUrl = hasCustomRange
|
||||
? API_URLS.graphLogVillages(villageId, period, dateFrom, dateTo)
|
||||
: API_URLS.graphLogVillages(villageId, period)
|
||||
|
||||
const { data: response, isLoading } = useSWR(apiUrl, fetcher)
|
||||
|
||||
const labels: Record<ChartPeriod, string> = {
|
||||
daily: 'Daily (last 14 days)',
|
||||
@@ -79,7 +89,6 @@ function ActivityChart({ villageId }: { villageId: string }) {
|
||||
|
||||
const rawData: any[] = Array.isArray(response?.data) ? response.data : []
|
||||
|
||||
// Normalize: map any field names from external API → { label, activity }
|
||||
const data = rawData.map((item) => {
|
||||
const label = item.label
|
||||
const activity = item.aktivitas
|
||||
@@ -95,21 +104,37 @@ function ActivityChart({ villageId }: { villageId: string }) {
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="sm">Village Activity Log</Text>
|
||||
<Text size="xs" c="dimmed">{labels[period]}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{hasCustomRange ? `${dateFrom} — ${dateTo}` : labels[period]}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<SegmentedControl
|
||||
value={period}
|
||||
onChange={(v) => setPeriod(v as ChartPeriod)}
|
||||
size="xs"
|
||||
radius="md"
|
||||
data={[
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'monthly', label: 'Monthly' },
|
||||
{ value: 'yearly', label: 'Yearly' },
|
||||
]}
|
||||
/>
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
placeholder="Pick date range"
|
||||
size="xs"
|
||||
radius="md"
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
clearable
|
||||
w={200}
|
||||
/>
|
||||
{!hasCustomRange && (
|
||||
<SegmentedControl
|
||||
value={period}
|
||||
onChange={(v) => setPeriod(v as ChartPeriod)}
|
||||
size="xs"
|
||||
radius="md"
|
||||
data={[
|
||||
{ value: 'daily', label: 'Daily' },
|
||||
{ value: 'monthly', label: 'Monthly' },
|
||||
{ value: 'yearly', label: 'Yearly' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -168,6 +193,64 @@ function ActivityChart({ villageId }: { villageId: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Recent Activity Logs ──────────────────────────────────────────────────────
|
||||
|
||||
function RecentVillageLogs({ villageId }: { villageId: string }) {
|
||||
const { data: response, isLoading } = useSWR(API_URLS.getRecentVillageLogs(villageId), fetcher)
|
||||
const logs: any[] = Array.isArray(response?.data) ? response.data : []
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="xl" p="lg">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="teal">
|
||||
<TbClock size={14} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="sm">Recent Activity</Text>
|
||||
<Text size="xs" c="dimmed">Latest user actions in this village</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{isLoading ? (
|
||||
<Stack h={120} align="center" justify="center">
|
||||
<Loader type="dots" />
|
||||
</Stack>
|
||||
) : logs.length === 0 ? (
|
||||
<Text size="sm" c="dimmed" ta="center" py="md">No recent activity.</Text>
|
||||
) : (
|
||||
<Table verticalSpacing="xs" className="data-table">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Time</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th>Action</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{logs.map((log: any, i: number) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>
|
||||
<Text size="xs">{dayjs(log.timestamp).format('D MMM YYYY, HH:mm')}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm" fw={500}>{log.userName || 'Unknown'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs">{log.action || '-'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>{log.desc || '-'}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function VillageDetailPage() {
|
||||
@@ -474,21 +557,22 @@ function VillageDetailPage() {
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* ── Chart + Info Panels ── */}
|
||||
{/* ── Activity Chart ── */}
|
||||
<ActivityChart villageId={villageId} />
|
||||
|
||||
{/* ── Recent Logs + System Info ── */}
|
||||
<Box
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '3fr 1fr',
|
||||
gridTemplateColumns: '2fr 1fr',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
{/* Left (3/4): Activity Chart */}
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<ActivityChart villageId={villageId} />
|
||||
<RecentVillageLogs villageId={villageId} />
|
||||
</Box>
|
||||
|
||||
{/* Right (1/4): Informasi Sistem */}
|
||||
<Paper withBorder radius="xl" p="lg">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="teal">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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,
|
||||
@@ -37,8 +39,10 @@ import { useEffect, useState } from 'react'
|
||||
import {
|
||||
TbAlertTriangle,
|
||||
TbBug,
|
||||
TbChartBar,
|
||||
TbCircleCheck,
|
||||
TbCircleX,
|
||||
TbClock,
|
||||
TbDeviceDesktop,
|
||||
TbDeviceMobile,
|
||||
TbFilter,
|
||||
@@ -46,7 +50,9 @@ import {
|
||||
TbPhoto,
|
||||
TbPlus,
|
||||
TbSearch,
|
||||
TbTrendingUp,
|
||||
} from 'react-icons/tb'
|
||||
import useSWR from 'swr'
|
||||
|
||||
export const Route = createFileRoute('/bug-reports')({
|
||||
component: ListErrorsPage,
|
||||
@@ -77,6 +83,7 @@ function ListErrorsPage() {
|
||||
const [status, setStatus] = useState('all')
|
||||
const [source, setSource] = useState('all')
|
||||
const [dateRange, setDateRange] = useState<DatesRangeValue>([null, null])
|
||||
const [bugRange, setBugRange] = useState<7 | 30 | 90>(7)
|
||||
|
||||
const [debouncedSearch] = useDebouncedValue(search, 400)
|
||||
|
||||
@@ -103,6 +110,8 @@ function ListErrorsPage() {
|
||||
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()),
|
||||
@@ -247,6 +256,177 @@ function ListErrorsPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{/* Bug Statistics Section */}
|
||||
{bugStats && (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
|
||||
<SummaryCard
|
||||
title="Total Bugs"
|
||||
value={bugStats.totalBugs?.toLocaleString() ?? '0'}
|
||||
icon={TbBug}
|
||||
color="brand-blue"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Open Bugs"
|
||||
value={bugStats.openBugs?.toLocaleString() ?? '0'}
|
||||
icon={TbAlertTriangle}
|
||||
color="red"
|
||||
isError={bugStats.openBugs > 0}
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Avg Resolution Time"
|
||||
value={`${bugStats.avgResolutionHours ?? 0}h`}
|
||||
icon={TbClock}
|
||||
color="orange"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Resolution Rate"
|
||||
value={`${bugStats.resolutionRate ?? 0}%`}
|
||||
icon={TbTrendingUp}
|
||||
color="teal"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{bugStats && (
|
||||
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
|
||||
<Paper withBorder radius="2xl" className="glass" p="md">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="brand-blue">
|
||||
<TbChartBar size={14} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="sm">Bugs per Application</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<BarChart
|
||||
h={220}
|
||||
data={(bugStats.byApp || []).map((item: { appId: string; count: number }) => ({
|
||||
...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 (
|
||||
<div style={{
|
||||
backgroundColor: '#1A1B1E',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #373A40',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 600, color: '#C1C2C5', marginBottom: '4px' }}>
|
||||
{payload[0]?.payload?.appId}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#2563EB' }}>
|
||||
Bugs: <span style={{ fontWeight: 700 }}>{payload[0]?.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="bugBarGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#2563EB" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#7C3AED" stopOpacity={0.8} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</BarChart>
|
||||
</Paper>
|
||||
<Paper withBorder radius="2xl" className="glass" p="md">
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="violet">
|
||||
<TbTrendingUp size={14} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={700} size="sm">Bug Trend</Text>
|
||||
<Text size="xs" c="dimmed">Last {bugRange} days</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap={4}>
|
||||
{([7, 30, 90] as const).map((r) => (
|
||||
<Button
|
||||
key={r}
|
||||
size="compact-xs"
|
||||
variant={bugRange === r ? 'filled' : 'subtle'}
|
||||
color="violet"
|
||||
radius="md"
|
||||
onClick={() => setBugRange(r)}
|
||||
>
|
||||
{r === 7 ? '7D' : r === 30 ? '1M' : '3M'}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Group>
|
||||
<AreaChart
|
||||
h={220}
|
||||
data={bugStats.trend || []}
|
||||
dataKey="date"
|
||||
series={[{ name: 'count', color: '#7C3AED' }]}
|
||||
curveType="monotone"
|
||||
withTooltip
|
||||
tickLine="none"
|
||||
gridAxis="x"
|
||||
fillOpacity={0.3}
|
||||
xAxisProps={{
|
||||
interval: bugRange === 7 ? 0 : bugRange === 30 ? 4 : 9,
|
||||
tick: { fontSize: 10, fill: '#909296' },
|
||||
angle: bugRange === 7 ? 0 : -45,
|
||||
textAnchor: 'end',
|
||||
height: bugRange === 7 ? 30 : 60,
|
||||
}}
|
||||
tooltipProps={{
|
||||
content: ({ active, payload }: any) => {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div style={{
|
||||
backgroundColor: '#1A1B1E',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #373A40',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.5)',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 600, color: '#C1C2C5', marginBottom: '4px' }}>
|
||||
{payload[0]?.payload?.date}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#7C3AED' }}>
|
||||
Bugs: <span style={{ fontWeight: 700 }}>{payload[0]?.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
'.recharts-area-curve': {
|
||||
strokeWidth: 2.5,
|
||||
filter: 'drop-shadow(0 3px 6px rgba(124, 58, 237, 0.3))',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{/* Image Preview Modal */}
|
||||
<Modal
|
||||
opened={!!previewImage}
|
||||
|
||||
Reference in New Issue
Block a user