Compare commits
2 Commits
test-model
...
test-pener
| Author | SHA1 | Date | |
|---|---|---|---|
| a791efe76c | |||
| e9f7bc2043 |
@@ -5,7 +5,7 @@ function Summary({ title, data }: any) {
|
||||
if (!data || data.length === 0) return null;
|
||||
|
||||
const totalAnggaran = data.reduce((s: number, i: any) => s + i.anggaran, 0);
|
||||
const totalRealisasi = data.reduce((s: number, i: any) => s + i.realisasi, 0);
|
||||
const totalRealisasi = data.reduce((s: number, i: any) => s + i.totalRealisasi, 0);
|
||||
|
||||
const persen =
|
||||
totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
||||
@@ -36,38 +36,38 @@ function Summary({ title, data }: any) {
|
||||
{persen.toFixed(2)}%
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
|
||||
<Text fz="sm" c="dimmed" mb="xs">
|
||||
Realisasi: {formatRupiah(totalRealisasi)} / Anggaran: {formatRupiah(totalAnggaran)}
|
||||
</Text>
|
||||
|
||||
<Progress
|
||||
value={persen}
|
||||
size="xl"
|
||||
|
||||
<Progress
|
||||
value={persen}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={getProgressColor(persen)}
|
||||
striped={persen < 100}
|
||||
animated={persen < 100}
|
||||
/>
|
||||
|
||||
|
||||
{persen >= 100 && (
|
||||
<Text fz="xs" c="teal" mt="xs" fw={500}>
|
||||
✓ Realisasi mencapai 100% dari anggaran
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{persen < 100 && persen >= 80 && (
|
||||
<Text fz="xs" c="blue" mt="xs" fw={500}>
|
||||
⚡ Realisasi baik, mendekati target
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{persen < 80 && persen >= 60 && (
|
||||
<Text fz="xs" c="yellow" mt="xs" fw={500}>
|
||||
⚠️ Realisasi cukup, perlu ditingkatkan
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{persen < 60 && (
|
||||
<Text fz="xs" c="red" mt="xs" fw={500}>
|
||||
⚠️ Realisasi rendah, perlu perhatian khusus
|
||||
@@ -87,7 +87,7 @@ export default function GrafikRealisasi({ apbdesData }: any) {
|
||||
|
||||
// Hitung total keseluruhan
|
||||
const totalAnggaranSemua = items.reduce((s: number, i: any) => s + i.anggaran, 0);
|
||||
const totalRealisasiSemua = items.reduce((s: number, i: any) => s + i.realisasi, 0);
|
||||
const totalRealisasiSemua = items.reduce((s: number, i: any) => s + i.totalRealisasi, 0);
|
||||
const persenSemua = totalAnggaranSemua > 0 ? (totalRealisasiSemua / totalAnggaranSemua) * 100 : 0;
|
||||
|
||||
const formatRupiah = (angka: number) => {
|
||||
@@ -105,30 +105,32 @@ export default function GrafikRealisasi({ apbdesData }: any) {
|
||||
GRAFIK REALISASI APBDes {tahun}
|
||||
</Title>
|
||||
|
||||
{/* Summary Total Keseluruhan */}
|
||||
<Box mb="lg" p="md" bg="gray.0" radius="md">
|
||||
<>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
||||
<Text fw={700} fz="xl" c={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}>
|
||||
{persenSemua.toFixed(2)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed" mb="xs">
|
||||
{formatRupiah(totalRealisasiSemua)} / {formatRupiah(totalAnggaranSemua)}
|
||||
</Text>
|
||||
<Progress
|
||||
value={persenSemua}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
|
||||
<Stack gap="lg">
|
||||
<Summary title="💰 Pendapatan" data={pendapatan} />
|
||||
<Summary title="💸 Belanja" data={belanja} />
|
||||
<Summary title="📊 Pembiayaan" data={pembiayaan} />
|
||||
</Stack>
|
||||
|
||||
{/* Summary Total Keseluruhan */}
|
||||
<Box mb="lg" p="md" bg="gray.0">
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
||||
<Text fw={700} fz="xl" c={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}>
|
||||
{persenSemua.toFixed(2)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz="sm" c="dimmed" mb="xs">
|
||||
{formatRupiah(totalRealisasiSemua)} / {formatRupiah(totalAnggaranSemua)}
|
||||
</Text>
|
||||
<Progress
|
||||
value={persenSemua}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color={persenSemua >= 100 ? 'teal' : persenSemua >= 80 ? 'blue' : 'red'}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user