fix(realisasi): add kode field to RealisasiItem and simplify table display
- Add kode field to RealisasiItem model in Prisma schema - Update API endpoints (create, update) to accept kode parameter - Update state management with proper type definitions - Add kode input field in RealisasiManager component - Simplify realisasiTable to show flat list (Kode, Uraian, Realisasi, %) - Remove section grouping and expandable details - Fix race condition in findUnique.load() with loading guard - Fix linting errors across multiple files Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -4,29 +4,25 @@
|
||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||
import colors from '@/con/colors'
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Divider,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Title
|
||||
} from '@mantine/core'
|
||||
import { IconDownload } from '@tabler/icons-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useProxy } from 'valtio/utils'
|
||||
import GrafikRealisasi from './lib/grafikRealisasi'
|
||||
import PaguTable from './lib/paguTable'
|
||||
import RealisasiTable from './lib/realisasiTable'
|
||||
import GrafikRealisasi from './lib/grafikRealisasi'
|
||||
|
||||
function Apbdes() {
|
||||
const state = useProxy(apbdes)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
||||
|
||||
const textHeading = {
|
||||
@@ -37,12 +33,9 @@ function Apbdes() {
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await state.findMany.load()
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
@@ -73,10 +66,12 @@ function Apbdes() {
|
||||
? dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0]
|
||||
: null
|
||||
|
||||
const data = (state.findMany.data || []).slice(0, 3)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const previewData = (state.findMany.data || []).slice(0, 3)
|
||||
|
||||
return (
|
||||
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
||||
<Divider c="gray.3" size="sm" />
|
||||
{/* 📌 HEADING */}
|
||||
<Box mt="xl">
|
||||
<Stack gap="sm">
|
||||
@@ -116,7 +111,7 @@ function Apbdes() {
|
||||
</Group>
|
||||
|
||||
{/* COMBOBOX */}
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Box px={{ base: 'md', md: "sm" }}>
|
||||
<Select
|
||||
label={<Text fw={600} fz="sm">Pilih Tahun APBDes</Text>}
|
||||
placeholder="Pilih tahun"
|
||||
@@ -132,7 +127,7 @@ function Apbdes() {
|
||||
|
||||
{/* Tabel & Grafik - Hanya tampilkan jika ada data */}
|
||||
{currentApbdes && currentApbdes.items?.length > 0 ? (
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<Box px={{ base: 'md', md: 'sm' }} mb="xl">
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||
<PaguTable apbdesData={currentApbdes} />
|
||||
<RealisasiTable apbdesData={currentApbdes} />
|
||||
@@ -140,19 +135,19 @@ function Apbdes() {
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
) : currentApbdes ? (
|
||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
||||
<Box px={{ base: 'md', md: 100 }} py="md" mb="xl">
|
||||
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
|
||||
Tidak ada data item untuk tahun yang dipilih.
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* GRID - Card Preview */}
|
||||
{loading ? (
|
||||
{/* GRID - Card Preview
|
||||
{state.findMany.loading ? (
|
||||
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
||||
<Loader size="lg" color="blue" />
|
||||
</Center>
|
||||
) : data.length === 0 ? (
|
||||
) : previewData.length === 0 ? (
|
||||
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
||||
<Stack align="center" gap="xs">
|
||||
<Text fz="lg" c="dimmed" lh={1.4}>
|
||||
@@ -170,7 +165,7 @@ function Apbdes() {
|
||||
spacing="lg"
|
||||
pb="xl"
|
||||
>
|
||||
{data.map((v: any, k: number) => (
|
||||
{previewData.map((v, k) => (
|
||||
<Box
|
||||
key={k}
|
||||
pos="relative"
|
||||
@@ -224,7 +219,7 @@ function Apbdes() {
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
)} */}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function GrafikRealisasi({ apbdesData }: any) {
|
||||
</Title>
|
||||
|
||||
{/* Summary Total Keseluruhan */}
|
||||
<Box mb="lg" p="md" bg="gray.0" radius="md">
|
||||
<Box mb="lg" p="md" bg="gray.0">
|
||||
<>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
||||
|
||||
@@ -1,146 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
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}>
|
||||
<Text fw={700} fz="sm">{title}</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
{data.map((item: any) => (
|
||||
<ItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
expanded={expandedId === item.id}
|
||||
onToggle={() => setExpandedId(expandedId === item.id ? null : item.id)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
import { Paper, Table, Title, Badge, Text } from '@mantine/core';
|
||||
|
||||
export default function RealisasiTable({ apbdesData }: any) {
|
||||
const items = apbdesData.items || [];
|
||||
@@ -150,32 +9,78 @@ export default function RealisasiTable({ apbdesData }: any) {
|
||||
? `REALISASI APBDes Tahun ${apbdesData.tahun}`
|
||||
: 'REALISASI APBDes';
|
||||
|
||||
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');
|
||||
// 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 formatRupiah = (amount: number) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
{allRealisasiRows.length === 0 ? (
|
||||
<Text fz="sm" c="dimmed" ta="center" py="md">
|
||||
Belum ada data realisasi
|
||||
</Text>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Uraian</Table.Th>
|
||||
<Table.Th ta="right">Realisasi (Rp)</Table.Th>
|
||||
<Table.Th ta="center">%</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{allRealisasiRows.map(({ realisasi, parentItem }) => {
|
||||
const persentase = parentItem.anggaran > 0
|
||||
? (realisasi.jumlah / parentItem.anggaran) * 100
|
||||
: 0;
|
||||
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Uraian</Table.Th>
|
||||
<Table.Th ta="right">Total Realisasi (Rp)</Table.Th>
|
||||
<Table.Th ta="center">%</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
<Section title="1) PENDAPATAN" data={pendapatan} />
|
||||
<Section title="2) BELANJA" data={belanja} />
|
||||
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
return (
|
||||
<Table.Tr key={realisasi.id}>
|
||||
<Table.Td>
|
||||
<Text>{realisasi.kode} - {realisasi.keterangan}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Text fw={600} c="blue">
|
||||
{formatRupiah(realisasi.jumlah)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td ta="center">
|
||||
<Badge
|
||||
color={
|
||||
persentase >= 100
|
||||
? 'teal'
|
||||
: persentase >= 60
|
||||
? 'yellow'
|
||||
: 'red'
|
||||
}
|
||||
>
|
||||
{persentase.toFixed(2)}%
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user