Compare commits
7 Commits
amalia/25-
...
e82443ee03
| Author | SHA1 | Date | |
|---|---|---|---|
| e82443ee03 | |||
| 501fbde118 | |||
| fe4ddf686e | |||
| fe83fd6025 | |||
| 457f36be06 | |||
| 5002fd1519 | |||
| 8aaec351cf |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bun-react-template",
|
||||
"version": "0.1.16",
|
||||
"version": "0.1.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
@@ -74,7 +73,7 @@ export const ErrorDataTable = forwardRef<ErrorDataTableHandle, ErrorDataTablePro
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper withBorder radius="2xl" className="glass" style={{ overflow: 'hidden' }}>
|
||||
<Paper withBorder radius="2xl" className="glass" style={{ overflowX: 'auto' }}>
|
||||
<Box p="lg" style={{ borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<Group justify="space-between">
|
||||
<Group gap="sm">
|
||||
@@ -101,15 +100,15 @@ export const ErrorDataTable = forwardRef<ErrorDataTableHandle, ErrorDataTablePro
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<ScrollArea>
|
||||
<Table.ScrollContainer minWidth={520}>
|
||||
<Table verticalSpacing="sm" highlightOnHover className="data-table">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th px="lg">Error Description</Table.Th>
|
||||
<Table.Th>Reporter</Table.Th>
|
||||
<Table.Th>Version</Table.Th>
|
||||
<Table.Th>Reported</Table.Th>
|
||||
<Table.Th pr="lg">Status</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Reporter</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Version</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Reported</Table.Th>
|
||||
<Table.Th pr="lg" style={{ whiteSpace: 'nowrap' }}>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -149,8 +148,8 @@ export const ErrorDataTable = forwardRef<ErrorDataTableHandle, ErrorDataTablePro
|
||||
v{error.affectedVersion || 'N/A'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap={4}>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<TbHistory size={12} color="gray" />
|
||||
<Text size="xs" c="dimmed">
|
||||
{dayjs(error.createdAt).format('D MMM YYYY, HH:mm')}
|
||||
@@ -170,7 +169,7 @@ export const ErrorDataTable = forwardRef<ErrorDataTableHandle, ErrorDataTablePro
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</Table.ScrollContainer>
|
||||
</Paper>
|
||||
|
||||
<Drawer
|
||||
|
||||
@@ -463,27 +463,29 @@ function AppErrorsPage() {
|
||||
}}
|
||||
>
|
||||
<Accordion.Control>
|
||||
<Group wrap="nowrap">
|
||||
<Group wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<ThemeIcon
|
||||
color={STATUS_COLOR[bug.status] ?? 'gray'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
radius="md"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<TbAlertTriangle size={20} />
|
||||
</ThemeIcon>
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={600} lineClamp={1}>{bug.description}</Text>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text size="sm" fw={600} lineClamp={1} style={{ flex: 1, minWidth: 0 }}>{bug.description}</Text>
|
||||
<Badge
|
||||
color={STATUS_COLOR[bug.status] ?? 'gray'}
|
||||
variant="dot"
|
||||
size="sm"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{STATUS_LABEL[bug.status] ?? bug.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{dayjs(bug.createdAt).format('D MMM YYYY, HH:mm')} · {bug.appId?.toUpperCase()} · v{bug.affectedVersion}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
@@ -218,34 +219,36 @@ function RecentVillageLogs({ villageId }: { villageId: string }) {
|
||||
) : 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.ScrollContainer minWidth={380}>
|
||||
<Table verticalSpacing="xs" className="data-table">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Time</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Action</Table.Th>
|
||||
<Table.Th>Description</Table.Th>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{logs.map((log: any, i: number) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<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 style={{ whiteSpace: 'nowrap' }}>
|
||||
<Text size="xs">{log.action || '-'}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="xs" c="dimmed">{log.desc || '-'}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
@@ -561,47 +564,42 @@ function VillageDetailPage() {
|
||||
<ActivityChart villageId={villageId} />
|
||||
|
||||
{/* ── Recent Logs + System Info ── */}
|
||||
<Box
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '2fr 1fr',
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Grid gutter="md" align="flex-start">
|
||||
<Grid.Col span={{ base: 12, md: 8 }}>
|
||||
<RecentVillageLogs villageId={villageId} />
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
|
||||
<Paper withBorder radius="xl" p="lg">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="teal">
|
||||
<TbCalendar size={14} />
|
||||
</ThemeIcon>
|
||||
<Text fw={700} size="sm">System Information</Text>
|
||||
</Group>
|
||||
<Stack gap={0}>
|
||||
{[
|
||||
{ label: 'Date Created', value: village.createdAt },
|
||||
{ label: 'Created By', value: '-' },
|
||||
{ label: 'Last Updated', value: village.updatedAt },
|
||||
].map((item, idx, arr) => (
|
||||
<Group
|
||||
key={item.label}
|
||||
justify="space-between"
|
||||
py="xs"
|
||||
wrap="wrap"
|
||||
style={{
|
||||
borderBottom: idx < arr.length - 1 ? '1px solid var(--mantine-color-default-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Text size="xs" c="dimmed">{item.label}</Text>
|
||||
<Text size="xs" fw={600} ta="right">{item.value}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Grid.Col span={{ base: 12, md: 4 }}>
|
||||
<Paper withBorder radius="xl" p="lg">
|
||||
<Group gap="xs" mb="md">
|
||||
<ThemeIcon size={28} radius="md" variant="light" color="teal">
|
||||
<TbCalendar size={14} />
|
||||
</ThemeIcon>
|
||||
<Text fw={700} size="sm">System Information</Text>
|
||||
</Group>
|
||||
<Stack gap={0}>
|
||||
{[
|
||||
{ label: 'Date Created', value: village.createdAt },
|
||||
{ label: 'Created By', value: '-' },
|
||||
{ label: 'Last Updated', value: village.updatedAt },
|
||||
].map((item, idx, arr) => (
|
||||
<Group
|
||||
key={item.label}
|
||||
justify="space-between"
|
||||
py="xs"
|
||||
wrap="wrap"
|
||||
style={{
|
||||
borderBottom: idx < arr.length - 1 ? '1px solid var(--mantine-color-default-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Text size="xs" c="dimmed">{item.label}</Text>
|
||||
<Text size="xs" fw={600} ta="right">{item.value}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* ── Confirmation Modal ── */}
|
||||
<Modal
|
||||
|
||||
@@ -700,27 +700,29 @@ function ListErrorsPage() {
|
||||
}}
|
||||
>
|
||||
<Accordion.Control>
|
||||
<Group wrap="nowrap">
|
||||
<Group wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<ThemeIcon
|
||||
color={STATUS_COLOR[bug.status] ?? 'gray'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
radius="md"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<TbAlertTriangle size={20} />
|
||||
</ThemeIcon>
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" fw={600} lineClamp={1}>{bug.description}</Text>
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text size="sm" fw={600} lineClamp={1} style={{ flex: 1, minWidth: 0 }}>{bug.description}</Text>
|
||||
<Badge
|
||||
color={STATUS_COLOR[bug.status] ?? 'gray'}
|
||||
variant="dot"
|
||||
size="sm"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{STATUS_LABEL[bug.status] ?? bug.status}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" c="dimmed" lineClamp={1}>
|
||||
{dayjs(bug.createdAt).format('D MMM YYYY, HH:mm')} · {bug.appId?.toUpperCase()} · v{bug.affectedVersion}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -198,15 +198,15 @@ function DashboardPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Paper withBorder radius="2xl" className="glass" p="md">
|
||||
<Table className="data-table" verticalSpacing="sm">
|
||||
<Paper withBorder radius="2xl" className="glass" p="md" style={{ overflowX: 'auto' }}>
|
||||
<Table className="data-table" verticalSpacing="sm" style={{ minWidth: 560 }}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>App</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>App</Table.Th>
|
||||
<Table.Th>Error Message</Table.Th>
|
||||
<Table.Th>Version</Table.Th>
|
||||
<Table.Th>Reported</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Version</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Reported</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -227,7 +227,7 @@ function DashboardPage() {
|
||||
</Table.Tr>
|
||||
) : recentErrors.map((error: any) => (
|
||||
<Table.Tr key={error.id}>
|
||||
<Table.Td>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<Text fw={600} size="sm" tt="uppercase">{error.app}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td style={{ maxWidth: 280 }}>
|
||||
@@ -237,13 +237,13 @@ function DashboardPage() {
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<Badge variant="light" color="gray" size="sm">v{error.version}</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<Text size="xs" c="dimmed">{formatTimeAgo(error.time)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Table.Td style={{ whiteSpace: 'nowrap' }}>
|
||||
<Badge
|
||||
color={SEVERITY_COLOR[error.severity] ?? 'gray'}
|
||||
variant="light"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
@@ -100,39 +101,43 @@ function GlobalLogsPage() {
|
||||
</Group>
|
||||
|
||||
<Paper withBorder radius="xl" p="md" className="glass">
|
||||
<Group gap="sm" wrap="wrap" align="flex-end">
|
||||
<Select
|
||||
label="User"
|
||||
placeholder="All users"
|
||||
value={operatorId}
|
||||
onChange={(v) => { setOperatorId(v ?? 'all'); setPage(1) }}
|
||||
data={operatorOptions}
|
||||
w={200}
|
||||
clearable
|
||||
size="sm"
|
||||
/>
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
label="Date range"
|
||||
placeholder="Pick a date range"
|
||||
value={dateRange}
|
||||
onChange={(v) => { setDateRange(v); setPage(1) }}
|
||||
locale="id"
|
||||
valueFormat="DD MMM YYYY"
|
||||
clearable
|
||||
w={280}
|
||||
size="sm"
|
||||
/>
|
||||
<Stack gap="md">
|
||||
<Group gap="sm" wrap="wrap" align="flex-end">
|
||||
<Select
|
||||
label="User"
|
||||
placeholder="All users"
|
||||
value={operatorId}
|
||||
onChange={(v) => { setOperatorId(v ?? 'all'); setPage(1) }}
|
||||
data={operatorOptions}
|
||||
style={{ flex: 1, minWidth: 160 }}
|
||||
clearable
|
||||
size="sm"
|
||||
/>
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
label="Date range"
|
||||
placeholder="Pick a date range"
|
||||
value={dateRange}
|
||||
onChange={(v) => { setDateRange(v); setPage(1) }}
|
||||
locale="id"
|
||||
valueFormat="DD MMM YYYY"
|
||||
clearable
|
||||
style={{ flex: 2, minWidth: 220 }}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
<Stack gap={4}>
|
||||
<Text size="xs" fw={500} c="dimmed">Action type</Text>
|
||||
<SegmentedControl
|
||||
value={type}
|
||||
onChange={(v) => { setType(v); setPage(1) }}
|
||||
size="sm"
|
||||
data={LOG_TYPES.map((t) => ({ label: LOG_TYPE_LABEL[t] ?? t, value: t }))}
|
||||
/>
|
||||
<Box style={{ overflowX: 'auto' }}>
|
||||
<SegmentedControl
|
||||
value={type}
|
||||
onChange={(v) => { setType(v); setPage(1) }}
|
||||
size="sm"
|
||||
data={LOG_TYPES.map((t) => ({ label: LOG_TYPE_LABEL[t] ?? t, value: t }))}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{isLoading && !data ? (
|
||||
|
||||
@@ -309,14 +309,15 @@ function UsersPage() {
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Paper withBorder radius="2xl" className="glass" p={0} style={{ overflow: 'hidden' }}>
|
||||
<Paper withBorder radius="2xl" className="glass" p={0} style={{ overflowX: 'auto' }}>
|
||||
<Table.ScrollContainer minWidth={480}>
|
||||
<Table className="data-table" verticalSpacing="md" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Name & Contact</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Joined</Table.Th>
|
||||
<Table.Th>Actions</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Role</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Joined</Table.Th>
|
||||
<Table.Th style={{ whiteSpace: 'nowrap' }}>Actions</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -341,8 +342,8 @@ function UsersPage() {
|
||||
operators.map((user: any) => (
|
||||
<Table.Tr key={user.id}>
|
||||
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1 }}>
|
||||
<Group gap="sm">
|
||||
<Box style={{ position: 'relative' }}>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Box style={{ position: 'relative', flexShrink: 0 }}>
|
||||
<Avatar
|
||||
size="sm"
|
||||
radius="xl"
|
||||
@@ -384,7 +385,7 @@ function UsersPage() {
|
||||
{ROLE_LABEL[user.role] ?? user.role}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1 }}>
|
||||
<Table.Td style={{ opacity: user.active === false ? 0.45 : 1, whiteSpace: 'nowrap' }}>
|
||||
<Text size="xs" fw={500} c={user.active === false ? 'dimmed' : undefined}>
|
||||
{new Date(user.createdAt).toLocaleDateString('en-GB', {
|
||||
day: 'numeric',
|
||||
@@ -440,6 +441,7 @@ function UsersPage() {
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</Paper>
|
||||
|
||||
{response?.totalPages > 1 && (
|
||||
|
||||
Reference in New Issue
Block a user