feat(ui landing): add expandable realisasi detail in RealisasiTable

Features:
- Add expandable rows for each APBDes item
- Show detailed realisasi breakdown per item
- Each realisasi shows:
  * Keterangan/Uraian
  * Jumlah (formatted in Rupiah)
  * Tanggal (formatted date)
- Chevron icon indicator (right/down)
- Click row to expand/collapse
- Hover effect on clickable rows
- Info text: "Klik pada item untuk melihat detail realisasi"

UI Components:
- RealisasiDetail: Component to display list of realisasi
- ItemRow: Expandable row with click handler
- Updated Section: Manage expanded state per item

Styling:
- Gray background for detail section
- Blue color for amount
- Dimmed color for date
- Responsive layout with wrap="nowrap"
- Proper spacing between items

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-03 16:38:15 +08:00
parent 6712da9ac2
commit e9f7bc2043

View File

@@ -1,39 +1,142 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Paper, Table, Title, Badge } from '@mantine/core';
import { Paper, Table, Title, Badge, Group, Text, Box, Collapse } from '@mantine/core';
import { useState } from 'react';
import { IconChevronDown, IconChevronRight } from '@tabler/icons-react';
function RealisasiDetail({ realisasiItems }: { realisasiItems: any[] }) {
if (!realisasiItems || realisasiItems.length === 0) {
return (
<Text fz="xs" c="dimmed" py="xs">
Belum ada realisasi
</Text>
);
}
const formatRupiah = (amount: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
};
return (
<Box mt="xs" p="xs" bg="gray.0" radius="md">
<Text fz="xs" fw={600} mb="xs">
Rincian Realisasi ({realisasiItems.length}):
</Text>
<>
{realisasiItems.map((realisasi: any, idx: number) => (
<Box key={realisasi.id} mb={idx < realisasiItems.length - 1 ? 'xs' : 0}>
<Group justify="space-between" gap="xs" wrap="nowrap">
<Text fz="xs" style={{ flex: 1 }}>
{realisasi.keterangan || `Realisasi ${idx + 1}`}
</Text>
<Text fz="xs" fw={600} c="blue">
{formatRupiah(realisasi.jumlah)}
</Text>
<Text fz="xs" c="dimmed">
{formatDate(realisasi.tanggal)}
</Text>
</Group>
</Box>
))}
</>
</Box>
);
}
function ItemRow({ item, expanded, onToggle }: any) {
const formatRupiah = (amount: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
const hasRealisasi = item.realisasiItems && item.realisasiItems.length > 0;
return (
<>
<Table.Tr
onClick={onToggle}
style={{ cursor: 'pointer', '&:hover': { backgroundColor: 'var(--mantine-color-gray-0)' } }}
>
<Table.Td>
<Group gap="xs">
{hasRealisasi ? (
expanded ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)
) : (
<Box w={16} />
)}
<Text fw={500}>{item.kode} - {item.uraian}</Text>
</Group>
</Table.Td>
<Table.Td ta="right">
<Text fw={600} c="blue">
{formatRupiah(item.totalRealisasi)}
</Text>
</Table.Td>
<Table.Td ta="center">
<Badge
color={
item.persentase >= 100
? 'teal'
: item.persentase >= 60
? 'yellow'
: 'red'
}
>
{item.persentase.toFixed(2)}%
</Badge>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td colSpan={3} p={0}>
<Collapse in={expanded}>
{hasRealisasi && <RealisasiDetail realisasiItems={item.realisasiItems} />}
</Collapse>
</Table.Td>
</Table.Tr>
</>
);
}
function Section({ title, data }: any) {
if (!data || data.length === 0) return null;
const [expandedId, setExpandedId] = useState<string | null>(null);
return (
<>
<Table.Tr>
<Table.Td colSpan={3}>
<strong>{title}</strong>
<Text fw={700} fz="sm">{title}</Text>
</Table.Td>
</Table.Tr>
{data.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td>
{item.kode} - {item.uraian}
</Table.Td>
<Table.Td ta="right">
Rp {item.totalRealisasi.toLocaleString('id-ID')}
</Table.Td>
<Table.Td ta="center">
<Badge
color={
item.persentase >= 100
? 'teal'
: item.persentase >= 60
? 'yellow'
: 'red'
}
>
{item.persentase.toFixed(2)}%
</Badge>
</Table.Td>
</Table.Tr>
<ItemRow
key={item.id}
item={item}
expanded={expandedId === item.id}
onToggle={() => setExpandedId(expandedId === item.id ? null : item.id)}
/>
))}
</>
);
@@ -55,11 +158,15 @@ export default function RealisasiTable({ apbdesData }: any) {
<Paper withBorder p="md" radius="md">
<Title order={5} mb="md">{title}</Title>
<Text fz="xs" c="dimmed" mb="sm">
💡 Klik pada item untuk melihat detail realisasi
</Text>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Uraian</Table.Th>
<Table.Th ta="right">Realisasi (Rp)</Table.Th>
<Table.Th ta="right">Total Realisasi (Rp)</Table.Th>
<Table.Th ta="center">%</Table.Th>
</Table.Tr>
</Table.Thead>