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:
@@ -238,6 +238,7 @@ model APBDesItem {
|
|||||||
// Model baru untuk multiple realisasi per item
|
// Model baru untuk multiple realisasi per item
|
||||||
model RealisasiItem {
|
model RealisasiItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
kode String? // Kode realisasi, mirip dengan APBDesItem
|
||||||
apbdesItemId String
|
apbdesItemId String
|
||||||
apbdesItem APBDesItem @relation(fields: [apbdesItemId], references: [id], onDelete: Cascade)
|
apbdesItem APBDesItem @relation(fields: [apbdesItemId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@ -251,6 +252,7 @@ model RealisasiItem {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([kode])
|
||||||
@@index([apbdesItemId])
|
@@index([apbdesItemId])
|
||||||
@@index([tanggal])
|
@@index([tanggal])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,6 @@ const ApbdesItemSchema = z.object({
|
|||||||
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
tipe: z.enum(['pendapatan', 'belanja', 'pembiayaan']).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Zod Schema untuk Realisasi Item ---
|
|
||||||
const RealisasiItemSchema = z.object({
|
|
||||||
jumlah: z.number().min(0, "Jumlah tidak boleh negatif"),
|
|
||||||
tanggal: z.string(),
|
|
||||||
keterangan: z.string().optional(),
|
|
||||||
buktiFileId: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ApbdesFormSchema = z.object({
|
const ApbdesFormSchema = z.object({
|
||||||
tahun: z.number().int().min(2000, "Tahun tidak valid"),
|
tahun: z.number().int().min(2000, "Tahun tidak valid"),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -157,7 +149,7 @@ const apbdes = proxy({
|
|||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| Prisma.APBDesGetPayload<{
|
| Prisma.APBDesGetPayload<{
|
||||||
include: { image: true; file: true; items: true };
|
include: { image: true; file: true; items: { include: { realisasiItems: true } } };
|
||||||
}>
|
}>
|
||||||
| null,
|
| null,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -170,15 +162,19 @@ const apbdes = proxy({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent multiple simultaneous loads
|
||||||
|
if (this.loading) {
|
||||||
|
console.log("⚠️ Already loading, skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pastikan URL-nya benar
|
|
||||||
const url = `/api/landingpage/apbdes/${id}`;
|
const url = `/api/landingpage/apbdes/${id}`;
|
||||||
console.log("🌐 Fetching:", url);
|
console.log("🌐 Fetching:", url);
|
||||||
|
|
||||||
// Gunakan fetch biasa atau ApiFetch dengan cara yang benar
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const res = await response.json();
|
const res = await response.json();
|
||||||
|
|
||||||
@@ -322,15 +318,16 @@ const apbdes = proxy({
|
|||||||
// =========================================
|
// =========================================
|
||||||
realisasi: {
|
realisasi: {
|
||||||
// Create realisasi
|
// Create realisasi
|
||||||
async create(itemId: string, data: { jumlah: number; tanggal: string; keterangan?: string; buktiFileId?: string }) {
|
async create(itemId: string, data: { kode: string; jumlah: number; tanggal: string; keterangan?: string; buktiFileId?: string }) {
|
||||||
try {
|
try {
|
||||||
const res = await (ApiFetch.api.landingpage.apbdes as any)[itemId].realisasi.post(data);
|
const res = await (ApiFetch.api.landingpage.apbdes as any)[itemId].realisasi.post(data);
|
||||||
|
|
||||||
if (res.data?.success) {
|
if (res.data?.success) {
|
||||||
toast.success("Realisasi berhasil ditambahkan");
|
toast.success("Realisasi berhasil ditambahkan");
|
||||||
// Reload findUnique untuk update data
|
// Reload findUnique untuk update data
|
||||||
if (apbdes.findUnique.data) {
|
const currentId = apbdes.findUnique.data?.id;
|
||||||
await apbdes.findUnique.load(apbdes.findUnique.data.id);
|
if (currentId) {
|
||||||
|
await apbdes.findUnique.load(currentId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@@ -345,15 +342,16 @@ const apbdes = proxy({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Update realisasi
|
// Update realisasi
|
||||||
async update(realisasiId: string, data: { jumlah?: number; tanggal?: string; keterangan?: string; buktiFileId?: string }) {
|
async update(realisasiId: string, data: { kode?: string; jumlah?: number; tanggal?: string; keterangan?: string; buktiFileId?: string }) {
|
||||||
try {
|
try {
|
||||||
const res = await (ApiFetch.api.landingpage.apbdes as any).realisasi[realisasiId].put(data);
|
const res = await (ApiFetch.api.landingpage.apbdes as any).realisasi[realisasiId].put(data);
|
||||||
|
|
||||||
if (res.data?.success) {
|
if (res.data?.success) {
|
||||||
toast.success("Realisasi berhasil diperbarui");
|
toast.success("Realisasi berhasil diperbarui");
|
||||||
// Reload findUnique untuk update data
|
// Reload findUnique untuk update data
|
||||||
if (apbdes.findUnique.data) {
|
const currentId = apbdes.findUnique.data?.id;
|
||||||
await apbdes.findUnique.load(apbdes.findUnique.data.id);
|
if (currentId) {
|
||||||
|
await apbdes.findUnique.load(currentId);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -23,7 +25,6 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Modal,
|
Modal,
|
||||||
Divider,
|
Divider,
|
||||||
Loader,
|
|
||||||
Center,
|
Center,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
@@ -33,9 +34,6 @@ import {
|
|||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCoin,
|
IconCoin,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
|
|
||||||
interface RealisasiManagerProps {
|
interface RealisasiManagerProps {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
@@ -63,6 +61,7 @@ export default function RealisasiManager({
|
|||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
kode: '',
|
||||||
jumlah: 0,
|
jumlah: 0,
|
||||||
tanggal: new Date().toISOString().split('T')[0], // YYYY-MM-DD format for input
|
tanggal: new Date().toISOString().split('T')[0], // YYYY-MM-DD format for input
|
||||||
keterangan: '',
|
keterangan: '',
|
||||||
@@ -70,6 +69,7 @@ export default function RealisasiManager({
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
kode: '',
|
||||||
jumlah: 0,
|
jumlah: 0,
|
||||||
tanggal: new Date().toISOString().split('T')[0],
|
tanggal: new Date().toISOString().split('T')[0],
|
||||||
keterangan: '',
|
keterangan: '',
|
||||||
@@ -87,6 +87,7 @@ export default function RealisasiManager({
|
|||||||
const tanggalStr = tanggal.toISOString().split('T')[0]; // YYYY-MM-DD
|
const tanggalStr = tanggal.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
|
kode: realisasi.kode || '',
|
||||||
jumlah: realisasi.jumlah,
|
jumlah: realisasi.jumlah,
|
||||||
tanggal: tanggalStr,
|
tanggal: tanggalStr,
|
||||||
keterangan: realisasi.keterangan || '',
|
keterangan: realisasi.keterangan || '',
|
||||||
@@ -100,12 +101,17 @@ export default function RealisasiManager({
|
|||||||
return toast.warn('Jumlah realisasi harus lebih dari 0');
|
return toast.warn('Jumlah realisasi harus lebih dari 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!formData.kode || formData.kode.trim() === '') {
|
||||||
|
return toast.warn('Kode realisasi wajib diisi');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
// Update existing realisasi
|
// Update existing realisasi
|
||||||
const success = await state.realisasi.update(editingId, {
|
const success = await state.realisasi.update(editingId, {
|
||||||
|
kode: formData.kode,
|
||||||
jumlah: formData.jumlah,
|
jumlah: formData.jumlah,
|
||||||
tanggal: new Date(formData.tanggal).toISOString(),
|
tanggal: new Date(formData.tanggal).toISOString(),
|
||||||
keterangan: formData.keterangan,
|
keterangan: formData.keterangan,
|
||||||
@@ -117,6 +123,7 @@ export default function RealisasiManager({
|
|||||||
} else {
|
} else {
|
||||||
// Create new realisasi
|
// Create new realisasi
|
||||||
const success = await state.realisasi.create(itemId, {
|
const success = await state.realisasi.create(itemId, {
|
||||||
|
kode: formData.kode,
|
||||||
jumlah: formData.jumlah,
|
jumlah: formData.jumlah,
|
||||||
tanggal: new Date(formData.tanggal).toISOString(),
|
tanggal: new Date(formData.tanggal).toISOString(),
|
||||||
keterangan: formData.keterangan,
|
keterangan: formData.keterangan,
|
||||||
@@ -257,6 +264,7 @@ export default function RealisasiManager({
|
|||||||
<Table striped highlightOnHover fz="sm">
|
<Table striped highlightOnHover fz="sm">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
|
<TableTh>Kode</TableTh>
|
||||||
<TableTh>Tanggal</TableTh>
|
<TableTh>Tanggal</TableTh>
|
||||||
<TableTh>Uraian</TableTh>
|
<TableTh>Uraian</TableTh>
|
||||||
<TableTh ta="right">Jumlah</TableTh>
|
<TableTh ta="right">Jumlah</TableTh>
|
||||||
@@ -266,6 +274,11 @@ export default function RealisasiManager({
|
|||||||
<TableTbody>
|
<TableTbody>
|
||||||
{realisasiItems.map((realisasi) => (
|
{realisasiItems.map((realisasi) => (
|
||||||
<TableTr key={realisasi.id}>
|
<TableTr key={realisasi.id}>
|
||||||
|
<TableTd>
|
||||||
|
<Badge variant="light" color="blue" size="sm">
|
||||||
|
{realisasi.kode || '-'}
|
||||||
|
</Badge>
|
||||||
|
</TableTd>
|
||||||
<TableTd>
|
<TableTd>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconCalendar size={16} />
|
<IconCalendar size={16} />
|
||||||
@@ -314,7 +327,7 @@ export default function RealisasiManager({
|
|||||||
Belum ada realisasi untuk item ini
|
Belum ada realisasi untuk item ini
|
||||||
</Text>
|
</Text>
|
||||||
<Text fz="xs" c="dimmed">
|
<Text fz="xs" c="dimmed">
|
||||||
Klik tombol "Tambah Realisasi" untuk menambahkan
|
Klik tombol "Tambah Realisasi" untuk menambahkan
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -349,6 +362,15 @@ export default function RealisasiManager({
|
|||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Kode Realisasi"
|
||||||
|
placeholder="Contoh: 4.1.1-R1"
|
||||||
|
value={formData.kode}
|
||||||
|
onChange={(e) => setFormData({ ...formData, kode: e.target.value })}
|
||||||
|
description="Kode unik untuk realisasi ini"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label="Jumlah Realisasi (Rp)"
|
label="Jumlah Realisasi (Rp)"
|
||||||
value={formData.jumlah}
|
value={formData.jumlah}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ function DetailAPBDes() {
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{/* Realisasi Manager untuk setiap item */}
|
{/* Realisasi Manager untuk setiap item */}
|
||||||
{data.items.map((item: any) => (
|
{data.items.map((item) => (
|
||||||
<RealisasiManager
|
<RealisasiManager
|
||||||
key={item.id}
|
key={item.id}
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const APBDes = new Elysia({
|
|||||||
.post("/:itemId/realisasi", realisasiCreate, {
|
.post("/:itemId/realisasi", realisasiCreate, {
|
||||||
params: t.Object({ itemId: t.String() }),
|
params: t.Object({ itemId: t.String() }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
|
kode: t.String(),
|
||||||
jumlah: t.Number(),
|
jumlah: t.Number(),
|
||||||
tanggal: t.String(),
|
tanggal: t.String(),
|
||||||
keterangan: t.Optional(t.String()),
|
keterangan: t.Optional(t.String()),
|
||||||
@@ -80,6 +81,7 @@ const APBDes = new Elysia({
|
|||||||
.put("/realisasi/:realisasiId", realisasiUpdate, {
|
.put("/realisasi/:realisasiId", realisasiUpdate, {
|
||||||
params: t.Object({ realisasiId: t.String() }),
|
params: t.Object({ realisasiId: t.String() }),
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
|
kode: t.Optional(t.String()),
|
||||||
jumlah: t.Optional(t.Number()),
|
jumlah: t.Optional(t.Number()),
|
||||||
tanggal: t.Optional(t.String()),
|
tanggal: t.Optional(t.String()),
|
||||||
keterangan: t.Optional(t.String()),
|
keterangan: t.Optional(t.String()),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
type RealisasiCreateBody = {
|
type RealisasiCreateBody = {
|
||||||
|
kode: string;
|
||||||
jumlah: number;
|
jumlah: number;
|
||||||
tanggal: string; // ISO format
|
tanggal: string; // ISO format
|
||||||
keterangan?: string;
|
keterangan?: string;
|
||||||
@@ -33,6 +34,7 @@ export default async function realisasiCreate(context: Context) {
|
|||||||
const realisasi = await prisma.realisasiItem.create({
|
const realisasi = await prisma.realisasiItem.create({
|
||||||
data: {
|
data: {
|
||||||
apbdesItemId: itemId,
|
apbdesItemId: itemId,
|
||||||
|
kode: body.kode,
|
||||||
jumlah: body.jumlah,
|
jumlah: body.jumlah,
|
||||||
tanggal: new Date(body.tanggal),
|
tanggal: new Date(body.tanggal),
|
||||||
keterangan: body.keterangan,
|
keterangan: body.keterangan,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import prisma from "@/lib/prisma";
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
type RealisasiUpdateBody = {
|
type RealisasiUpdateBody = {
|
||||||
|
kode?: string;
|
||||||
jumlah?: number;
|
jumlah?: number;
|
||||||
tanggal?: string;
|
tanggal?: string;
|
||||||
keterangan?: string;
|
keterangan?: string;
|
||||||
@@ -33,6 +34,7 @@ export default async function realisasiUpdate(context: Context) {
|
|||||||
const updated = await prisma.realisasiItem.update({
|
const updated = await prisma.realisasiItem.update({
|
||||||
where: { id: realisasiId },
|
where: { id: realisasiId },
|
||||||
data: {
|
data: {
|
||||||
|
...(body.kode !== undefined && { kode: body.kode }),
|
||||||
...(body.jumlah !== undefined && { jumlah: body.jumlah }),
|
...(body.jumlah !== undefined && { jumlah: body.jumlah }),
|
||||||
...(body.tanggal !== undefined && { tanggal: new Date(body.tanggal) }),
|
...(body.tanggal !== undefined && { tanggal: new Date(body.tanggal) }),
|
||||||
...(body.keterangan !== undefined && { keterangan: body.keterangan }),
|
...(body.keterangan !== undefined && { keterangan: body.keterangan }),
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const MusicPlayer = () => {
|
|||||||
}
|
}
|
||||||
// Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0)
|
// Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0)
|
||||||
}
|
}
|
||||||
}, [currentSong?.id]); // Intentional: hanya depend on song ID, bukan isPlaying
|
}, [currentSong?.id]); // eslint-disable-line react-hooks/exhaustive-deps -- Intentional: hanya depend on song ID, bukan isPlaying
|
||||||
|
|
||||||
// Sync duration dari audio element jika berbeda signifikan (> 1 detik)
|
// Sync duration dari audio element jika berbeda signifikan (> 1 detik)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -155,7 +155,7 @@ const MusicPlayer = () => {
|
|||||||
|
|
||||||
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
}, [currentSong?.id]); // Intentional: hanya depend on song ID
|
}, [currentSong?.id]); // eslint-disable-line react-hooks/exhaustive-deps -- Intentional: hanya depend on song ID
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
|
|||||||
@@ -4,29 +4,25 @@
|
|||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title
|
||||||
} from '@mantine/core'
|
} from '@mantine/core'
|
||||||
import { IconDownload } from '@tabler/icons-react'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
|
import GrafikRealisasi from './lib/grafikRealisasi'
|
||||||
import PaguTable from './lib/paguTable'
|
import PaguTable from './lib/paguTable'
|
||||||
import RealisasiTable from './lib/realisasiTable'
|
import RealisasiTable from './lib/realisasiTable'
|
||||||
import GrafikRealisasi from './lib/grafikRealisasi'
|
|
||||||
|
|
||||||
function Apbdes() {
|
function Apbdes() {
|
||||||
const state = useProxy(apbdes)
|
const state = useProxy(apbdes)
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
||||||
|
|
||||||
const textHeading = {
|
const textHeading = {
|
||||||
@@ -37,12 +33,9 @@ function Apbdes() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
|
||||||
await state.findMany.load()
|
await state.findMany.load()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error)
|
console.error('Error loading data:', error)
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadData()
|
loadData()
|
||||||
@@ -73,10 +66,12 @@ function Apbdes() {
|
|||||||
? dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0]
|
? dataAPBDes.find((item: any) => item?.tahun?.toString() === selectedYear) || dataAPBDes[0]
|
||||||
: null
|
: 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 (
|
return (
|
||||||
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
<Stack p="sm" gap="xl" bg={colors.Bg}>
|
||||||
|
<Divider c="gray.3" size="sm" />
|
||||||
{/* 📌 HEADING */}
|
{/* 📌 HEADING */}
|
||||||
<Box mt="xl">
|
<Box mt="xl">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
@@ -116,7 +111,7 @@ function Apbdes() {
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* COMBOBOX */}
|
{/* COMBOBOX */}
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: "sm" }}>
|
||||||
<Select
|
<Select
|
||||||
label={<Text fw={600} fz="sm">Pilih Tahun APBDes</Text>}
|
label={<Text fw={600} fz="sm">Pilih Tahun APBDes</Text>}
|
||||||
placeholder="Pilih tahun"
|
placeholder="Pilih tahun"
|
||||||
@@ -132,7 +127,7 @@ function Apbdes() {
|
|||||||
|
|
||||||
{/* Tabel & Grafik - Hanya tampilkan jika ada data */}
|
{/* Tabel & Grafik - Hanya tampilkan jika ada data */}
|
||||||
{currentApbdes && currentApbdes.items?.length > 0 ? (
|
{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 }}>
|
<SimpleGrid cols={{ base: 1, sm: 3 }}>
|
||||||
<PaguTable apbdesData={currentApbdes} />
|
<PaguTable apbdesData={currentApbdes} />
|
||||||
<RealisasiTable apbdesData={currentApbdes} />
|
<RealisasiTable apbdesData={currentApbdes} />
|
||||||
@@ -140,19 +135,19 @@ function Apbdes() {
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Box>
|
</Box>
|
||||||
) : currentApbdes ? (
|
) : 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}>
|
<Text fz="sm" c="dimmed" ta="center" lh={1.5}>
|
||||||
Tidak ada data item untuk tahun yang dipilih.
|
Tidak ada data item untuk tahun yang dipilih.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* GRID - Card Preview */}
|
{/* GRID - Card Preview
|
||||||
{loading ? (
|
{state.findMany.loading ? (
|
||||||
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
||||||
<Loader size="lg" color="blue" />
|
<Loader size="lg" color="blue" />
|
||||||
</Center>
|
</Center>
|
||||||
) : data.length === 0 ? (
|
) : previewData.length === 0 ? (
|
||||||
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
<Center mx={{ base: 'md', md: 100 }} mih={200} pb="xl">
|
||||||
<Stack align="center" gap="xs">
|
<Stack align="center" gap="xs">
|
||||||
<Text fz="lg" c="dimmed" lh={1.4}>
|
<Text fz="lg" c="dimmed" lh={1.4}>
|
||||||
@@ -170,7 +165,7 @@ function Apbdes() {
|
|||||||
spacing="lg"
|
spacing="lg"
|
||||||
pb="xl"
|
pb="xl"
|
||||||
>
|
>
|
||||||
{data.map((v: any, k: number) => (
|
{previewData.map((v, k) => (
|
||||||
<Box
|
<Box
|
||||||
key={k}
|
key={k}
|
||||||
pos="relative"
|
pos="relative"
|
||||||
@@ -224,7 +219,7 @@ function Apbdes() {
|
|||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)} */}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function GrafikRealisasi({ apbdesData }: any) {
|
|||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{/* Summary Total Keseluruhan */}
|
{/* 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">
|
<Group justify="space-between" mb="xs">
|
||||||
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
<Text fw={700} fz="lg">TOTAL KESELURUHAN</Text>
|
||||||
|
|||||||
@@ -1,146 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Paper, Table, Title, Badge, Group, Text, Box, Collapse } from '@mantine/core';
|
import { Paper, Table, Title, Badge, Text } 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)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RealisasiTable({ apbdesData }: any) {
|
export default function RealisasiTable({ apbdesData }: any) {
|
||||||
const items = apbdesData.items || [];
|
const items = apbdesData.items || [];
|
||||||
@@ -150,32 +9,78 @@ export default function RealisasiTable({ apbdesData }: any) {
|
|||||||
? `REALISASI APBDes Tahun ${apbdesData.tahun}`
|
? `REALISASI APBDes Tahun ${apbdesData.tahun}`
|
||||||
: 'REALISASI APBDes';
|
: 'REALISASI APBDes';
|
||||||
|
|
||||||
const pendapatan = items.filter((i: any) => i.tipe === 'pendapatan');
|
// Flatten: kumpulkan semua realisasi items
|
||||||
const belanja = items.filter((i: any) => i.tipe === 'belanja');
|
const allRealisasiRows: Array<{ realisasi: any; parentItem: any }> = [];
|
||||||
const pembiayaan = items.filter((i: any) => i.tipe === 'pembiayaan');
|
|
||||||
|
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 (
|
return (
|
||||||
<Paper withBorder p="md" radius="md">
|
<Paper withBorder p="md" radius="md">
|
||||||
<Title order={5} mb="md">{title}</Title>
|
<Title order={5} mb="md">{title}</Title>
|
||||||
|
|
||||||
<Text fz="xs" c="dimmed" mb="sm">
|
{allRealisasiRows.length === 0 ? (
|
||||||
💡 Klik pada item untuk melihat detail realisasi
|
<Text fz="sm" c="dimmed" ta="center" py="md">
|
||||||
</Text>
|
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>
|
return (
|
||||||
<Table.Thead>
|
<Table.Tr key={realisasi.id}>
|
||||||
<Table.Tr>
|
<Table.Td>
|
||||||
<Table.Th>Uraian</Table.Th>
|
<Text>{realisasi.kode} - {realisasi.keterangan}</Text>
|
||||||
<Table.Th ta="right">Total Realisasi (Rp)</Table.Th>
|
</Table.Td>
|
||||||
<Table.Th ta="center">%</Table.Th>
|
<Table.Td ta="right">
|
||||||
</Table.Tr>
|
<Text fw={600} c="blue">
|
||||||
</Table.Thead>
|
{formatRupiah(realisasi.jumlah)}
|
||||||
<Table.Tbody>
|
</Text>
|
||||||
<Section title="1) PENDAPATAN" data={pendapatan} />
|
</Table.Td>
|
||||||
<Section title="2) BELANJA" data={belanja} />
|
<Table.Td ta="center">
|
||||||
<Section title="3) PEMBIAYAAN" data={pembiayaan} />
|
<Badge
|
||||||
</Table.Tbody>
|
color={
|
||||||
</Table>
|
persentase >= 100
|
||||||
|
? 'teal'
|
||||||
|
: persentase >= 60
|
||||||
|
? 'yellow'
|
||||||
|
: 'red'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{persentase.toFixed(2)}%
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user