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:
2026-03-04 11:51:58 +08:00
parent a791efe76c
commit 2d901912ea
11 changed files with 144 additions and 216 deletions

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>
);
}