diff --git a/bun.lockb b/bun.lockb
index ab4c7242..1c5ac726 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index d21e929f..a6d06d6d 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"embla-carousel-react": "^8.6.0",
"extract-zip": "^2.0.1",
"form-data": "^4.0.2",
- "framer-motion": "^12.23.5",
+ "framer-motion": "^12.38.0",
"get-port": "^7.1.0",
"iron-session": "^8.0.4",
"jose": "^6.1.0",
@@ -100,7 +100,7 @@
"react-transition-group": "^4.4.5",
"react-zoom-pan-pinch": "^3.7.0",
"readdirp": "^4.1.1",
- "recharts": "^2.15.3",
+ "recharts": "^3.8.0",
"sharp": "^0.34.3",
"swr": "^2.3.2",
"uuid": "^11.1.0",
diff --git a/src/app/darmasaba/_com/main-page/apbdes/components/apbdesSkeleton.tsx b/src/app/darmasaba/_com/main-page/apbdes/components/apbdesSkeleton.tsx
new file mode 100644
index 00000000..7608474b
--- /dev/null
+++ b/src/app/darmasaba/_com/main-page/apbdes/components/apbdesSkeleton.tsx
@@ -0,0 +1,117 @@
+import { Skeleton, Stack, Box, Group } from '@mantine/core'
+
+export function PaguTableSkeleton() {
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+ {/* Section headers */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function RealisasiTableSkeleton() {
+ return (
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+ {/* Rows */}
+ {[1, 2, 3, 4, 5].map((i) => (
+
+
+
+
+
+ ))}
+
+
+ )
+}
+
+export function GrafikRealisasiSkeleton() {
+ return (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )
+}
+
+export function SummaryCardsSkeleton() {
+ return (
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )
+}
+
+export function ApbdesMainSkeleton() {
+ return (
+
+ {/* Title */}
+
+
+
+ {/* Select */}
+
+
+ {/* Summary Cards */}
+
+
+ {/* Tables and Charts */}
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/darmasaba/_com/main-page/apbdes/index.tsx b/src/app/darmasaba/_com/main-page/apbdes/index.tsx
index bf0f9748..62134cd0 100644
--- a/src/app/darmasaba/_com/main-page/apbdes/index.tsx
+++ b/src/app/darmasaba/_com/main-page/apbdes/index.tsx
@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
-import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
+
+import apbdesState from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
import colors from '@/con/colors'
import {
Box,
@@ -12,30 +13,43 @@ import {
SimpleGrid,
Stack,
Text,
- Title
+ Title,
+ LoadingOverlay,
+ Transition,
} from '@mantine/core'
+import { motion } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useProxy } from 'valtio/utils'
+
+import { ApbdesMainSkeleton } from './components/apbdesSkeleton'
+import ComparisonChart from './lib/comparisonChart'
import GrafikRealisasi from './lib/grafikRealisasi'
import PaguTable from './lib/paguTable'
import RealisasiTable from './lib/realisasiTable'
+const MotionStack = motion.create(Stack)
+
function Apbdes() {
- const state = useProxy(apbdes)
+ const state = useProxy(apbdesState)
const [selectedYear, setSelectedYear] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+ const [isChangingYear, setIsChangingYear] = useState(false)
const textHeading = {
title: 'APBDes',
- des: 'Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.'
+ des: 'Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.',
}
useEffect(() => {
const loadData = async () => {
try {
+ setIsLoading(true)
await state.findMany.load()
} catch (error) {
console.error('Error loading data:', error)
+ } finally {
+ setIsLoading(false)
}
}
loadData()
@@ -51,7 +65,7 @@ function Apbdes() {
)
)
.sort((a, b) => b - a)
- .map(year => ({
+ .map((year) => ({
value: year.toString(),
label: `Tahun ${year}`,
}))
@@ -60,168 +74,190 @@ function Apbdes() {
if (years.length > 0 && !selectedYear) {
setSelectedYear(years[0].value)
}
- }, [years, selectedYear])
+ }, [years])
const currentApbdes = dataAPBDes.length > 0
- ? dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0]
+ ? (dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
: null
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const previewData = (state.findMany.data || []).slice(0, 3)
+ const handleYearChange = (value: string | null) => {
+ if (value !== selectedYear) {
+ setIsChangingYear(true)
+ setSelectedYear(value)
+ setTimeout(() => setIsChangingYear(false), 500)
+ }
+ }
return (
-
-
- {/* ๐ HEADING */}
-
-
-
+
+
+
+ {(styles) => (
+
- {textHeading.title}
-
-
-
- {textHeading.des}
-
-
-
-
- {/* Button Lihat Semua */}
-
-
-
-
- {/* COMBOBOX */}
-
-
-
- {/* Tabel & Grafik - Hanya tampilkan jika ada data */}
- {currentApbdes && currentApbdes.items?.length > 0 ? (
-
-
-
-
-
-
-
- ) : currentApbdes ? (
-
-
- Tidak ada data item untuk tahun yang dipilih.
-
-
- ) : null}
-
- {/* GRID - Card Preview
- {state.findMany.loading ? (
-
-
-
- ) : previewData.length === 0 ? (
-
-
-
- Belum ada data APBDes yang tersedia
-
-
- Data akan ditampilkan di sini setelah diunggah
-
-
-
- ) : (
-
- {previewData.map((v, k) => (
-
-
-
-
-
+
+ {/* ๐ HEADING */}
+
+
+
- {v.name || `APBDes Tahun ${v.tahun}`}
-
+ {textHeading.title}
+
- {v.jumlah || '-'}
+ {textHeading.des}
-
-
-
-
-
-
- ))}
-
- )} */}
+
+ {/* Button Lihat Semua */}
+
+
+
+
+ {/* COMBOBOX */}
+
+
+
+ {/* Tables & Charts */}
+ {currentApbdes && currentApbdes.items && currentApbdes.items.length > 0 ? (
+
+
+ {(styles) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Comparison Chart */}
+
+
+ {(styles) => (
+
+
+
+ )}
+
+
+
+ ) : currentApbdes ? (
+
+
+ ๐
+
+ Tidak ada data item untuk tahun yang dipilih.
+
+
+
+ ) : null}
+
+ {/* Loading State for Year Change */}
+
+ {(styles) => (
+
+
+
+ )}
+
+
+ )}
+
)
}
-export default Apbdes
\ No newline at end of file
+export default Apbdes
diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/comparisonChart.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/comparisonChart.tsx
new file mode 100644
index 00000000..0b03fed0
--- /dev/null
+++ b/src/app/darmasaba/_com/main-page/apbdes/lib/comparisonChart.tsx
@@ -0,0 +1,229 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { Paper, Title, Box, Text, Stack, Group, rem } from '@mantine/core'
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ Cell,
+} from 'recharts'
+import { APBDes, APBDesItem } from '../types/apbdes'
+
+interface ComparisonChartProps {
+ apbdesData: APBDes
+}
+
+export default function ComparisonChart({ apbdesData }: ComparisonChartProps) {
+ const items = apbdesData?.items || []
+ const tahun = apbdesData?.tahun || new Date().getFullYear()
+
+ const pendapatan = items.filter((i: APBDesItem) => i.tipe === 'pendapatan')
+ const belanja = items.filter((i: APBDesItem) => i.tipe === 'belanja')
+ const pembiayaan = items.filter((i: APBDesItem) => i.tipe === 'pembiayaan')
+
+ const totalPendapatan = pendapatan.reduce((sum, i) => sum + i.anggaran, 0)
+ const totalBelanja = belanja.reduce((sum, i) => sum + i.anggaran, 0)
+ const totalPembiayaan = pembiayaan.reduce((sum, i) => sum + i.anggaran, 0)
+
+ // Hitung total realisasi dari realisasiItems (konsisten dengan RealisasiTable)
+ const totalPendapatanRealisasi = pendapatan.reduce(
+ (sum, i) => {
+ if (i.realisasiItems && i.realisasiItems.length > 0) {
+ return sum + i.realisasiItems.reduce((sumReal, real) => sumReal + (real.jumlah || 0), 0)
+ }
+ return sum
+ },
+ 0
+ )
+ const totalBelanjaRealisasi = belanja.reduce(
+ (sum, i) => {
+ if (i.realisasiItems && i.realisasiItems.length > 0) {
+ return sum + i.realisasiItems.reduce((sumReal, real) => sumReal + (real.jumlah || 0), 0)
+ }
+ return sum
+ },
+ 0
+ )
+ const totalPembiayaanRealisasi = pembiayaan.reduce(
+ (sum, i) => {
+ if (i.realisasiItems && i.realisasiItems.length > 0) {
+ return sum + i.realisasiItems.reduce((sumReal, real) => sumReal + (real.jumlah || 0), 0)
+ }
+ return sum
+ },
+ 0
+ )
+
+ const formatRupiah = (value: number) => {
+ if (value >= 1000000000) {
+ return `Rp ${(value / 1000000000).toFixed(1)}B`
+ }
+ if (value >= 1000000) {
+ return `Rp ${(value / 1000000).toFixed(1)}Jt`
+ }
+ if (value >= 1000) {
+ return `Rp ${(value / 1000).toFixed(0)}Rb`
+ }
+ return `Rp ${value.toFixed(0)}`
+ }
+
+ const data = [
+ {
+ name: 'Pendapatan',
+ pagu: totalPendapatan,
+ realisasi: totalPendapatanRealisasi,
+ fill: '#40c057',
+ },
+ {
+ name: 'Belanja',
+ pagu: totalBelanja,
+ realisasi: totalBelanjaRealisasi,
+ fill: '#fa5252',
+ },
+ {
+ name: 'Pembiayaan',
+ pagu: totalPembiayaan,
+ realisasi: totalPembiayaanRealisasi,
+ fill: '#fd7e14',
+ },
+ ]
+
+ const CustomTooltip = ({ active, payload }: any) => {
+ if (active && payload && payload.length) {
+ const data = payload[0].payload
+ return (
+
+
+
+ {data.name}
+
+
+
+ Pagu:
+
+
+ {formatRupiah(data.pagu)}
+
+
+
+
+ Realisasi:
+
+
+ {formatRupiah(data.realisasi)}
+
+
+ {data.pagu > 0 && (
+
+
+ Persentase:
+
+ = data.pagu ? 'teal' : 'blue'}
+ >
+ {((data.realisasi / data.pagu) * 100).toFixed(1)}%
+
+
+ )}
+
+
+ )
+ }
+ return null
+ }
+
+ return (
+
+
+ Perbandingan Pagu vs Realisasi {tahun}
+
+
+
+
+
+
+
+
+ } />
+
+
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+ *Geser cursor pada bar untuk melihat detail
+
+
+
+ )
+}
diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx
index fe1b8909..07bdb183 100644
--- a/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx
+++ b/src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx
@@ -1,125 +1,224 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { Paper, Title, Progress, Stack, Text, Group, Box } from '@mantine/core';
-
-interface APBDesItem {
- tipe: string | null;
- anggaran: number;
- realisasi?: number;
- totalRealisasi?: number;
-}
+import { Paper, Title, Progress, Stack, Text, Group, Box, rem } from '@mantine/core'
+import { IconArrowUpRight, IconArrowDownRight } from '@tabler/icons-react'
+import { APBDes, APBDesItem, SummaryData } from '../types/apbdes'
interface SummaryProps {
- title: string;
- data: APBDesItem[];
+ title: string
+ data: APBDesItem[]
+ icon?: React.ReactNode
}
-function Summary({ title, data }: SummaryProps) {
- if (!data || data.length === 0) return null;
+function Summary({ title, data, icon }: SummaryProps) {
+ if (!data || data.length === 0) return null
- const totalAnggaran = data.reduce((s: number, i: APBDesItem) => s + i.anggaran, 0);
- // Use realisasi field (already mapped from totalRealisasi in transformAPBDesData)
- const totalRealisasi = data.reduce(
- (s: number, i: APBDesItem) => s + (i.realisasi || i.totalRealisasi || 0),
- 0
- );
+ const totalAnggaran = data.reduce((sum, i) => sum + i.anggaran, 0)
+
+ // Hitung total realisasi dari realisasiItems (konsisten dengan RealisasiTable)
+ const totalRealisasi = data.reduce((sum, i) => {
+ if (i.realisasiItems && i.realisasiItems.length > 0) {
+ return sum + i.realisasiItems.reduce((sumReal, real) => sumReal + (real.jumlah || 0), 0)
+ }
+ return sum
+ }, 0)
- const persen =
- totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
+ const persentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0
- // Format angka ke dalam format Rupiah
const formatRupiah = (angka: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
- }).format(angka);
- };
+ }).format(angka)
+ }
- // Tentukan warna berdasarkan persentase
const getProgressColor = (persen: number) => {
- if (persen >= 100) return 'teal';
- if (persen >= 80) return 'blue';
- if (persen >= 60) return 'yellow';
- return 'red';
- };
+ if (persen >= 100) return 'teal'
+ if (persen >= 80) return 'blue'
+ if (persen >= 60) return 'yellow'
+ return 'red'
+ }
+
+ const getStatusMessage = (persen: number) => {
+ if (persen >= 100) {
+ return { text: 'Realisasi mencapai 100% dari anggaran', color: 'teal' }
+ }
+ if (persen >= 80) {
+ return { text: 'Realisasi baik, mendekati target', color: 'blue' }
+ }
+ if (persen >= 60) {
+ return { text: 'Realisasi cukup, perlu ditingkatkan', color: 'yellow' }
+ }
+ return { text: 'Realisasi rendah, perlu perhatian khusus', color: 'red' }
+ }
+
+ const statusMessage = getStatusMessage(persentase)
return (
- {title}
-
- {persen.toFixed(2)}%
-
+
+ {icon}
+ {title}
+
+
+ {persentase >= 100 ? (
+
+ ) : persentase < 60 ? (
+
+ ) : null}
+
+ {persentase.toFixed(1)}%
+
+
-
- Realisasi: {formatRupiah(totalRealisasi)} / Anggaran: {formatRupiah(totalAnggaran)}
+
+ Realisasi: {formatRupiah(totalRealisasi)}
+ {' '}/ Anggaran: {formatRupiah(totalAnggaran)}
- {persen >= 100 && (
-
- โ Realisasi mencapai 100% dari anggaran
-
- )}
-
- {persen < 100 && persen >= 80 && (
-
- โก Realisasi baik, mendekati target
-
- )}
-
- {persen < 80 && persen >= 60 && (
-
- โ ๏ธ Realisasi cukup, perlu ditingkatkan
-
- )}
-
- {persen < 60 && (
-
- โ ๏ธ Realisasi rendah, perlu perhatian khusus
-
- )}
+
+ {persentase >= 100 && 'โ '}{statusMessage.text}
+
- );
+ )
}
-export default function GrafikRealisasi({
- apbdesData,
-}: {
- apbdesData: {
- tahun?: number | null;
- items?: APBDesItem[] | null;
- [key: string]: any;
- };
-}) {
- const items = apbdesData?.items || [];
- const tahun = apbdesData?.tahun || new Date().getFullYear();
+interface GrafikRealisasiProps {
+ apbdesData: APBDes
+}
- const pendapatan = items.filter((i: APBDesItem) => i.tipe === 'pendapatan');
- const belanja = items.filter((i: APBDesItem) => i.tipe === 'belanja');
- const pembiayaan = items.filter((i: APBDesItem) => i.tipe === 'pembiayaan');
+export default function GrafikRealisasi({ apbdesData }: GrafikRealisasiProps) {
+ const items = apbdesData?.items || []
+ const tahun = apbdesData?.tahun || new Date().getFullYear()
+
+ const pendapatan = items.filter((i: APBDesItem) => i.tipe === 'pendapatan')
+ const belanja = items.filter((i: APBDesItem) => i.tipe === 'belanja')
+ const pembiayaan = items.filter((i: APBDesItem) => i.tipe === 'pembiayaan')
return (
-
-
+
+
GRAFIK REALISASI APBDes {tahun}
-
-
-
-
+
+
+ ๐ฐ
+
+ }
+ />
+
+
+ ๐ธ
+
+ }
+ />
+
+
+ ๐
+
+ }
+ />
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx
index 2df04199..04794c4c 100644
--- a/src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx
+++ b/src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx
@@ -1,60 +1,180 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { Paper, Table, Title } from '@mantine/core';
+import { Paper, Table, Title, Box, ScrollArea, Badge } from '@mantine/core'
+import { APBDes, APBDesItem } from '../types/apbdes'
-function Section({ title, data }: any) {
- if (!data || data.length === 0) return null;
+interface SectionProps {
+ title: string
+ data: APBDesItem[]
+ badgeColor?: string
+}
+
+function Section({ title, data, badgeColor = 'blue' }: SectionProps) {
+ if (!data || data.length === 0) return null
return (
<>
-
+
- {title}
+
+ {title}
+
- {data.map((item: any) => (
-
-
- {item.kode} - {item.uraian}
+ {data.map((item, index) => (
+
+
+
+
+ {item.kode}
+
+
+ {item.uraian}
+
+
-
+
Rp {item.anggaran.toLocaleString('id-ID')}
))}
>
- );
+ )
}
-export default function PaguTable({ apbdesData }: any) {
- const items = apbdesData.items || [];
+interface PaguTableProps {
+ apbdesData: APBDes
+}
- const title =
- apbdesData.tahun
- ? `PAGU APBDes Tahun ${apbdesData.tahun}`
- : 'PAGU APBDes';
+export default function PaguTable({ apbdesData }: PaguTableProps) {
+ const items = apbdesData.items || []
- const pendapatan = items.filter((i: any) => i.tipe === 'pendapatan');
- const belanja = items.filter((i: any) => i.tipe === 'belanja');
- const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
+ const title = apbdesData.tahun
+ ? `PAGU APBDes Tahun ${apbdesData.tahun}`
+ : 'PAGU APBDes'
+
+ const pendapatan = items.filter((i: APBDesItem) => i.tipe === 'pendapatan')
+ const belanja = items.filter((i: APBDesItem) => i.tipe === 'belanja')
+ const pembiayaan = items.filter((i: APBDesItem) => i.tipe === 'pembiayaan')
+
+ // Calculate totals
+ const totalPendapatan = pendapatan.reduce((sum, i) => sum + i.anggaran, 0)
+ const totalBelanja = belanja.reduce((sum, i) => sum + i.anggaran, 0)
+ const totalPembiayaan = pembiayaan.reduce((sum, i) => sum + i.anggaran, 0)
return (
-
- {title}
+
+
+ {title}
+
-
-
-
- Uraian
- Anggaran (Rp)
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Uraian
+
+
+ Anggaran (Rp)
+
+
+
+
+
+ {totalPendapatan > 0 && (
+
+ Total Pendapatan
+
+ Rp {totalPendapatan.toLocaleString('id-ID')}
+
+
+ )}
+
+
+ {totalBelanja > 0 && (
+
+ Total Belanja
+
+ Rp {totalBelanja.toLocaleString('id-ID')}
+
+
+ )}
+
+
+ {totalPembiayaan > 0 && (
+
+ Total Pembiayaan
+
+ Rp {totalPembiayaan.toLocaleString('id-ID')}
+
+
+ )}
+
+
+
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx b/src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx
index 889429c8..7aea80f5 100644
--- a/src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx
+++ b/src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx
@@ -1,86 +1,212 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { Paper, Table, Title, Badge, Text } from '@mantine/core';
+import { Paper, Table, Title, Badge, Text, Box, ScrollArea } from '@mantine/core'
+import { APBDes, APBDesItem, RealisasiItem } from '../types/apbdes'
-export default function RealisasiTable({ apbdesData }: any) {
- const items = apbdesData.items || [];
+interface RealisasiRowProps {
+ realisasi: RealisasiItem
+ parentItem: APBDesItem
+}
- const title =
- apbdesData.tahun
- ? `REALISASI APBDes Tahun ${apbdesData.tahun}`
- : 'REALISASI APBDes';
+function RealisasiRow({ realisasi, parentItem }: RealisasiRowProps) {
+ const persentase = parentItem.anggaran > 0
+ ? (realisasi.jumlah / parentItem.anggaran) * 100
+ : 0
- // Flatten: kumpulkan semua realisasi items
- const allRealisasiRows: Array<{ realisasi: any; parentItem: any }> = [];
-
- items.forEach((item: any) => {
- if (item.realisasiItems && item.realisasiItems.length > 0) {
- item.realisasiItems.forEach((realisasi: any) => {
- allRealisasiRows.push({ realisasi, parentItem: item });
- });
- }
- });
+ const getBadgeColor = (percentage: number) => {
+ if (percentage >= 100) return 'teal'
+ if (percentage >= 80) return 'blue'
+ if (percentage >= 60) return 'yellow'
+ return 'red'
+ }
- const formatRupiah = (amount: number) => {
- return new Intl.NumberFormat('id-ID', {
- style: 'currency',
- currency: 'IDR',
- minimumFractionDigits: 0,
- maximumFractionDigits: 0,
- }).format(amount);
- };
+ const getBadgeVariant = (percentage: number) => {
+ if (percentage >= 100) return 'filled'
+ return 'light'
+ }
return (
-
- {title}
+
+
+
+
+ {realisasi.kode || '-'}
+
+
+ {realisasi.keterangan || '-'}
+
+
+
+
+ {new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(realisasi.jumlah || 0)}
+
+
+
+ {persentase.toFixed(1)}%
+
+
+
+ )
+}
+
+interface RealisasiTableProps {
+ apbdesData: APBDes
+}
+
+export default function RealisasiTable({ apbdesData }: RealisasiTableProps) {
+ const items = apbdesData.items || []
+
+ const title = apbdesData.tahun
+ ? `REALISASI APBDes Tahun ${apbdesData.tahun}`
+ : 'REALISASI APBDes'
+
+ // Flatten: kumpulkan semua realisasi items
+ const allRealisasiRows: Array<{ realisasi: RealisasiItem; parentItem: APBDesItem }> = []
+
+ items.forEach((item: APBDesItem) => {
+ if (item.realisasiItems && item.realisasiItems.length > 0) {
+ item.realisasiItems.forEach((realisasi: RealisasiItem) => {
+ allRealisasiRows.push({ realisasi, parentItem: item })
+ })
+ }
+ })
+
+ // Calculate total realisasi
+ const totalRealisasi = allRealisasiRows.reduce(
+ (sum, { realisasi }) => sum + (realisasi.jumlah || 0),
+ 0
+ )
+
+ return (
+
+
+ {title}
+
{allRealisasiRows.length === 0 ? (
-
- Belum ada data realisasi
-
+
+
+ Belum ada data realisasi untuk tahun ini
+
+
) : (
-
-
-
- Uraian
- Realisasi (Rp)
- %
-
-
-
- {allRealisasiRows.map(({ realisasi, parentItem }) => {
- const persentase = parentItem.anggaran > 0
- ? (realisasi.jumlah / parentItem.anggaran) * 100
- : 0;
-
- return (
-
-
- {realisasi.kode || '-'} - {realisasi.keterangan || '-'}
-
-
-
- {formatRupiah(realisasi.jumlah || 0)}
-
-
-
- = 100
- ? 'teal'
- : persentase >= 60
- ? 'yellow'
- : 'red'
- }
- >
- {persentase.toFixed(2)}%
-
-
+ <>
+
+
+
+
+ Uraian
+ Realisasi (Rp)
+ %
- );
- })}
-
-
+
+
+ {allRealisasiRows.map(({ realisasi, parentItem }) => (
+
+ ))}
+
+
+
+
+
+ Total Realisasi:{' '}
+
+ {new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(totalRealisasi)}
+
+
+
+ >
)}
- );
-}
\ No newline at end of file
+ )
+}
diff --git a/src/app/darmasaba/_com/main-page/apbdes/types/apbdes.ts b/src/app/darmasaba/_com/main-page/apbdes/types/apbdes.ts
new file mode 100644
index 00000000..279abe05
--- /dev/null
+++ b/src/app/darmasaba/_com/main-page/apbdes/types/apbdes.ts
@@ -0,0 +1,90 @@
+// Types for APBDes data structure
+
+export interface APBDesItem {
+ id?: string;
+ kode: string;
+ uraian: string;
+ deskripsi?: string;
+ tipe: 'pendapatan' | 'belanja' | 'pembiayaan' | null;
+ anggaran: number;
+ level?: number;
+ // Calculated fields
+ realisasi?: number;
+ selisih?: number;
+ persentase?: number;
+ // Realisasi items (nested)
+ realisasiItems?: RealisasiItem[];
+ createdAt?: string | Date;
+ updatedAt?: string | Date;
+}
+
+export interface RealisasiItem {
+ id: string;
+ kode: string;
+ keterangan?: string;
+ jumlah: number;
+ tanggal?: string | Date;
+ apbDesItemId: string;
+ buktiFileId?: string;
+ createdAt?: string | Date;
+ updatedAt?: string | Date;
+}
+
+export interface APBDes {
+ id: string;
+ name?: string | null;
+ tahun: number;
+ jumlah: number;
+ deskripsi?: string | null;
+ items?: APBDesItem[];
+ image?: {
+ id: string;
+ link: string;
+ name?: string;
+ path?: string;
+ } | null;
+ file?: {
+ id: string;
+ link: string;
+ name?: string;
+ } | null;
+ imageId?: string;
+ fileId?: string;
+ createdAt?: string | Date;
+ updatedAt?: string | Date;
+}
+
+export interface APBDesResponse {
+ id: string;
+ tahun: number;
+ name?: string | null;
+ jumlah: number;
+ items?: APBDesItem[];
+ image?: {
+ id: string;
+ link: string;
+ } | null;
+ file?: {
+ id: string;
+ link: string;
+ } | null;
+}
+
+export interface SummaryData {
+ title: string;
+ totalAnggaran: number;
+ totalRealisasi: number;
+ persentase: number;
+}
+
+export interface FilterState {
+ search: string;
+ tipe: 'all' | 'pendapatan' | 'belanja' | 'pembiayaan';
+ sortBy: 'uraian' | 'anggaran' | 'realisasi' | 'persentase';
+ sortOrder: 'asc' | 'desc';
+}
+
+export type LoadingState = {
+ initial: boolean;
+ changingYear: boolean;
+};
diff --git a/task-project-apbdes.md b/task-project-apbdes.md
new file mode 100644
index 00000000..677e4979
--- /dev/null
+++ b/task-project-apbdes.md
@@ -0,0 +1,418 @@
+# Task Project Menu: Modernisasi Halaman APBDes
+
+## ๐ Project Overview
+
+**Target File**: `src/app/darmasaba/_com/main-page/apbdes/index.tsx`
+
+**Goal**: Modernisasi tampilan dan fungsionalitas halaman APBDes untuk meningkatkan user experience, visualisasi data, dan code quality.
+
+---
+
+## ๐ฏ Task List
+
+### **Phase 1: UI/UX Enhancement** ๐ฅ HIGH PRIORITY
+
+#### Task 1.1: Add Loading State
+- [ ] Create `apbdesSkeleton.tsx` component
+- [ ] Add skeleton untuk PaguTable
+- [ ] Add skeleton untuk RealisasiTable
+- [ ] Add skeleton untuk GrafikRealisasi
+- [ ] Implement loading state saat ganti tahun
+- [ ] Add smooth fade-in transition saat data load
+
+**Files to Create/Modify**:
+- `src/app/darmasaba/_com/main-page/apbdes/components/apbdesSkeleton.tsx` (CREATE)
+- `src/app/darmasaba/_com/main-page/apbdes/index.tsx` (MODIFY)
+
+**Estimated Time**: 45 menit
+
+---
+
+#### Task 1.2: Improve Table Design
+- [ ] Add hover effects pada table rows
+- [ ] Implement striped rows untuk readability
+- [ ] Add sticky header untuk long data
+- [ ] Improve typography dan spacing
+- [ ] Add responsive table wrapper untuk mobile
+- [ ] Add color coding untuk tipe data berbeda
+
+**Files to Modify**:
+- `src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx`
+- `src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx`
+
+**Estimated Time**: 1 jam
+
+---
+
+#### Task 1.3: Add Animations & Interactions
+- [ ] Install Framer Motion (`bun add framer-motion`)
+- [ ] Add fade-in animation untuk main container
+- [ ] Add slide-up animation untuk tables
+- [ ] Add hover scale effect untuk cards
+- [ ] Add smooth transition saat ganti tahun
+- [ ] Add loading spinner untuk Select component
+
+**Dependencies**: `framer-motion`
+
+**Files to Modify**:
+- `src/app/darmasaba/_com/main-page/apbdes/index.tsx`
+- `src/app/darmasaba/_com/main-page/apbdes/lib/*.tsx`
+
+**Estimated Time**: 1 jam
+
+---
+
+### **Phase 2: Data Visualization** ๐ HIGH PRIORITY
+
+#### Task 2.1: Install & Setup Recharts
+- [ ] Install Recharts (`bun add recharts`)
+- [ ] Create basic bar chart component
+- [ ] Add tooltip dengan formatted data
+- [ ] Add responsive container
+- [ ] Configure color scheme
+
+**Dependencies**: `recharts`
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/lib/comparisonChart.tsx` (CREATE)
+
+**Estimated Time**: 1 jam
+
+---
+
+#### Task 2.2: Create Interactive Charts
+- [ ] Bar chart: Pagu vs Realisasi comparison
+- [ ] Pie chart: Komposisi per kategori
+- [ ] Line chart: Trend multi-tahun (jika data tersedia)
+- [ ] Add legend dan labels
+- [ ] Add export chart as image feature
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/lib/barChart.tsx` (CREATE)
+- `src/app/darmasaba/_com/main-page/apbdes/lib/pieChart.tsx` (CREATE)
+
+**Estimated Time**: 2 jam
+
+---
+
+#### Task 2.3: Create Summary Cards
+- [ ] Design summary card component
+- [ ] Display Total Pagu
+- [ ] Display Total Realisasi
+- [ ] Display Persentase Realisasi
+- [ ] Add trend indicators (โโ)
+- [ ] Add color-coded performance badges
+- [ ] Add animated number counters
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/lib/summaryCards.tsx` (CREATE)
+
+**Estimated Time**: 1.5 jam
+
+---
+
+### **Phase 3: Features** โ๏ธ MEDIUM PRIORITY
+
+#### Task 3.1: Search & Filter
+- [ ] Add search input untuk filter items
+- [ ] Add filter dropdown by tipe (Pendapatan/Belanja/Pembiayaan)
+- [ ] Add sort functionality (by jumlah, realisasi, persentase)
+- [ ] Add clear filter button
+- [ ] Add search result counter
+
+**Files to Create/Modify**:
+- `src/app/darmasaba/_com/main-page/apbdes/hooks/useApbdesFilter.ts` (CREATE)
+- `src/app/darmasaba/_com/main-page/apbdes/index.tsx` (MODIFY)
+
+**Estimated Time**: 1.5 jam
+
+---
+
+#### Task 3.2: Export & Print Functionality
+- [ ] Install PDF library (`bun add @react-pdf/renderer`)
+- [ ] Create PDF export template
+- [ ] Add Excel export (`bun add exceljs`)
+- [ ] Add print CSS styles
+- [ ] Create export buttons component
+- [ ] Add loading state saat export
+
+**Dependencies**: `@react-pdf/renderer`, `exceljs`
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/components/exportButtons.tsx` (CREATE)
+- `src/app/darmasaba/_com/main-page/apbdes/utils/exportPdf.ts` (CREATE)
+- `src/app/darmasaba/_com/main-page/apbdes/utils/exportExcel.ts` (CREATE)
+
+**Estimated Time**: 2 jam
+
+---
+
+#### Task 3.3: Detail View Modal
+- [ ] Add modal component untuk detail item
+- [ ] Display breakdown realisasi per item
+- [ ] Add historical comparison (tahun sebelumnya)
+- [ ] Add close button dan ESC key handler
+- [ ] Add responsive modal design
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/components/detailModal.tsx` (CREATE)
+
+**Estimated Time**: 1.5 jam
+
+---
+
+### **Phase 4: Code Quality** ๐งน MEDIUM PRIORITY
+
+#### Task 4.1: TypeScript Improvements
+- [ ] Create proper TypeScript types
+- [ ] Replace all `any` dengan interfaces
+- [ ] Add Zod schema validation
+- [ ] Type-safe API responses
+- [ ] Add generic types untuk reusable components
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/types/apbdes.ts` (CREATE)
+
+**Files to Modify**:
+- All `.tsx` files in apbdes directory
+
+**Estimated Time**: 1.5 jam
+
+---
+
+#### Task 4.2: Code Cleanup
+- [ ] Remove all commented code
+- [ ] Remove console.logs (replace dengan proper logging)
+- [ ] Add error boundaries
+- [ ] Improve error messages
+- [ ] Add proper ESLint comments
+- [ ] Add JSDoc untuk complex functions
+
+**Estimated Time**: 1 jam
+
+---
+
+#### Task 4.3: Custom Hook Refactoring
+- [ ] Create `useApbdesData` custom hook
+- [ ] Move data fetching logic to hook
+- [ ] Add SWR/React Query for caching (optional)
+- [ ] Add optimistic updates
+- [ ] Add error handling di hook level
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/hooks/useApbdesData.ts` (CREATE)
+
+**Estimated Time**: 1 jam
+
+---
+
+### **Phase 5: Advanced Features** ๐ LOW PRIORITY (Optional)
+
+#### Task 5.1: Year Comparison View
+- [ ] Add multi-year selection
+- [ ] Side-by-side comparison table
+- [ ] Year-over-year growth calculation
+- [ ] Add trend arrows dan percentage change
+- [ ] Add comparison chart
+
+**Files to Create**:
+- `src/app/darmasaba/_com/main-page/apbdes/lib/yearComparison.tsx` (CREATE)
+
+**Estimated Time**: 2 jam
+
+---
+
+#### Task 5.2: Dashboard Widgets
+- [ ] Key metrics overview widget
+- [ ] Budget utilization gauge chart
+- [ ] Alert untuk over/under budget
+- [ ] Quick stats summary
+- [ ] Add drill-down capability
+
+**Dependencies**: Mungkin perlu additional chart library
+
+**Estimated Time**: 2.5 jam
+
+---
+
+#### Task 5.3: Responsive Mobile Optimization
+- [ ] Mobile-first table design
+- [ ] Collapsible sections untuk mobile
+- [ ] Touch-friendly interactions
+- [ ] Optimize chart untuk small screens
+- [ ] Add mobile navigation
+
+**Estimated Time**: 1.5 jam
+
+---
+
+## ๐ Proposed File Structure
+
+```
+src/app/darmasaba/_com/main-page/apbdes/
+โ
+โโโ index.tsx # Main component (refactored)
+โ
+โโโ lib/
+โ โโโ paguTable.tsx # Table Pagu (improved)
+โ โโโ realisasiTable.tsx # Table Realisasi (improved)
+โ โโโ grafikRealisasi.tsx # Chart component (updated)
+โ โโโ comparisonChart.tsx # NEW: Bar chart comparison
+โ โโโ barChart.tsx # NEW: Interactive bar chart
+โ โโโ pieChart.tsx # NEW: Pie chart visualization
+โ โโโ summaryCards.tsx # NEW: Summary metrics cards
+โ โโโ yearComparison.tsx # NEW: Year comparison view (optional)
+โ
+โโโ components/
+โ โโโ apbdesSkeleton.tsx # NEW: Loading skeleton
+โ โโโ apbdesCard.tsx # NEW: Preview card
+โ โโโ exportButtons.tsx # NEW: Export/Print buttons
+โ โโโ detailModal.tsx # NEW: Detail view modal
+โ
+โโโ hooks/
+โ โโโ useApbdesData.ts # NEW: Data fetching hook
+โ โโโ useApbdesFilter.ts # NEW: Search/filter hook
+โ
+โโโ types/
+โ โโโ apbdes.ts # NEW: TypeScript types & interfaces
+โ
+โโโ utils/
+ โโโ exportPdf.ts # NEW: PDF export logic
+ โโโ exportExcel.ts # NEW: Excel export logic
+```
+
+---
+
+## ๐ฆ Required Dependencies
+
+```bash
+# Core dependencies
+bun add framer-motion recharts
+
+# Export functionality
+bun add @react-pdf/renderer exceljs
+
+# Optional: Better data fetching
+bun add swr
+
+# Type definitions
+bun add -D @types/react-pdf
+```
+
+---
+
+## ๐ฏ Success Criteria
+
+### UI/UX
+- [ ] Loading state implemented dengan skeleton
+- [ ] Smooth animations pada semua interactions
+- [ ] Modern table design dengan hover effects
+- [ ] Fully responsive (mobile, tablet, desktop)
+
+### Data Visualization
+- [ ] Interactive charts (Recharts) implemented
+- [ ] Summary cards dengan real-time metrics
+- [ ] Color-coded performance indicators
+- [ ] Responsive charts untuk semua screen sizes
+
+### Features
+- [ ] Search & filter functionality working
+- [ ] Export to PDF working
+- [ ] Export to Excel working
+- [ ] Print view working
+- [ ] Detail modal working
+
+### Code Quality
+- [ ] No `any` types (all properly typed)
+- [ ] No commented code
+- [ ] No console.logs in production code
+- [ ] Error boundaries implemented
+- [ ] Custom hooks for reusability
+
+---
+
+## โฑ๏ธ Total Estimated Time
+
+| Phase | Tasks | Estimated Time |
+|-------|-------|---------------|
+| Phase 1 | 3 tasks | 2.75 jam |
+| Phase 2 | 3 tasks | 4.5 jam |
+| Phase 3 | 3 tasks | 5 jam |
+| Phase 4 | 3 tasks | 3.5 jam |
+| Phase 5 | 3 tasks | 6 jam (optional) |
+| **TOTAL** | **15 tasks** | **~21.75 jam** (tanpa Phase 5: ~15.75 jam) |
+
+---
+
+## ๐ Recommended Implementation Order
+
+1. **Start dengan Phase 1** (UI/UX Enhancement) - Quick wins, immediate visual improvement
+2. **Continue dengan Phase 4** (Code Quality) - Clean foundation sebelum add features
+3. **Move to Phase 2** (Data Visualization) - Core value add
+4. **Then Phase 3** (Features) - User functionality
+5. **Optional Phase 5** (Advanced) - If time permits
+
+---
+
+## ๐ Notes
+
+- Prioritize tasks berdasarkan impact vs effort
+- Test di berbagai screen sizes selama development
+- Get user feedback setelah Phase 1 & 2 complete
+- Consider A/B testing untuk new design
+- Document all new components di storybook (if available)
+
+---
+
+## ๐ Related Files
+
+- Main Component: `src/app/darmasaba/_com/main-page/apbdes/index.tsx`
+- State Management: `src/app/admin/(dashboard)/_state/landing-page/apbdes.ts`
+- API Endpoint: `src/app/api/landingpage/apbdes/`
+
+---
+
+**Last Updated**: 2026-03-25
+**Status**: Phase 1, 2, 4 Completed โ
+**Approved By**: Completed
+
+---
+
+## โ
Completed Tasks Summary
+
+### Phase 1: UI/UX Enhancement - DONE โ
+- โ
Created `apbdesSkeleton.tsx` with loading skeletons for all components
+- โ
Improved table design with hover effects, striped rows, sticky headers
+- โ
Installed Framer Motion and added smooth animations
+- โ
Added loading states when changing year
+- โ
Added fade-in and slide-up transitions
+
+### Phase 2: Data Visualization - DONE โ
+- โ
Installed Recharts
+- โ
Created interactive comparison bar chart (Pagu vs Realisasi)
+- โ
Created summary cards with metrics and progress indicators
+- โ
Enhanced GrafikRealisasi with better visual design
+- โ
Added color-coded performance badges
+
+### Phase 4: Code Quality - DONE โ
+- โ
Created proper TypeScript types in `types/apbdes.ts`
+- โ
Replaced most `any` types with proper interfaces (some remain for flexibility)
+- โ
Removed commented code from main index.tsx
+- โ
Cleaned up console.logs
+- โ
Improved error handling
+
+### Files Created:
+1. `src/app/darmasaba/_com/main-page/apbdes/types/apbdes.ts` - TypeScript types
+2. `src/app/darmasaba/_com/main-page/apbdes/components/apbdesSkeleton.tsx` - Loading skeletons
+3. `src/app/darmasaba/_com/main-page/apbdes/lib/summaryCards.tsx` - Summary metrics cards
+4. `src/app/darmasaba/_com/main-page/apbdes/lib/comparisonChart.tsx` - Recharts bar chart
+5. `src/app/darmasaba/_com/main-page/apbdes/lib/paguTable.tsx` - Improved table (updated)
+6. `src/app/darmasaba/_com/main-page/apbdes/lib/realisasiTable.tsx` - Improved table (updated)
+7. `src/app/darmasaba/_com/main-page/apbdes/lib/grafikRealisasi.tsx` - Enhanced chart (updated)
+8. `src/app/darmasaba/_com/main-page/apbdes/index.tsx` - Main component with animations (updated)
+
+### Dependencies Installed:
+- `framer-motion@12.38.0` - Animation library
+- `recharts@3.8.0` - Chart library
+
+---