diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9cf9c335..4f8cd740 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1282,25 +1282,22 @@ model DetailDataPengangguran { // ========================================= PADESA PENDAPATAN ASLI DESA ========================================= // model ApbDesa { - id String @id @default(uuid()) - tahun Int - pendapatan Pendapatan @relation(fields: [pendapatanId], references: [id]) - pendapatanId String - belanja Belanja @relation(fields: [belanjaId], references: [id]) - belanjaId String - pembiayaan Pembiayaan @relation(fields: [pembiayaanId], references: [id]) - pembiayaanId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime @default(now()) - isActive Boolean @default(true) + id String @id @default(uuid()) + tahun Int + pembiayaan Pembiayaan[] @relation("ApbDesaPembiayaan") + belanja Belanja[] @relation("ApbDesaBelanja") + pendapatan Pendapatan[] @relation("ApbDesaPendapatan") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime @default(now()) + isActive Boolean @default(true) } model Pendapatan { id String @id @default(uuid()) name String value Int - ApbDesa ApbDesa[] + ApbDesa ApbDesa[] @relation("ApbDesaPendapatan") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) @@ -1311,20 +1308,20 @@ model Belanja { id String @id @default(uuid()) name String value Int - ApbDesa ApbDesa[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) + ApbDesa ApbDesa[] @relation("ApbDesaBelanja") } model Pembiayaan { id String @id @default(uuid()) name String value Int - ApbDesa ApbDesa[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime @default(now()) isActive Boolean @default(true) + ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan") } diff --git a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts index 5df47719..1ece171f 100644 --- a/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts +++ b/src/app/admin/(dashboard)/_state/ekonomi/PADesa.ts @@ -6,17 +6,17 @@ import { proxy } from "valtio"; import { z } from "zod"; const templateApbDesa = z.object({ - tahun: z.number(), - pendapatanId: z.string(), - belanjaId: z.string(), - pembiayaanId: z.string(), + tahun: z.number().min(4, "Tahun minimal 4 karakter"), + pembiayaanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pembiayaan"), + belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"), + pendapatanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pendapatan"), }); const ApbDesaDefaultForm = { tahun: 0, - pendapatanId: "", - belanjaId: "", - pembiayaanId: "", + pendapatanIds: [] as string[], + belanjaIds: [] as string[], + pembiayaanIds: [] as string[], }; const ApbDesa = proxy({ @@ -54,13 +54,15 @@ const ApbDesa = proxy({ }, }, findMany: { - data: [] as Array<{ - id: string; - tahun: number; - pendapatanId: string; - belanjaId: string; - pembiayaanId: string; - }>, + data: null as + | Prisma.ApbDesaGetPayload<{ + include: { + pendapatan: true; + belanja: true; + pembiayaan: true; + }; + }>[] + | null, loading: false, async load() { try { @@ -105,21 +107,14 @@ const ApbDesa = proxy({ } const data = await response.json(); this.id = id; - this.form = data; + return data; } catch (error) { - console.error("Load error:", error); - toast.error("Gagal mengambil APB Desa"); + console.error("Error loading APB Desa:", error); + toast.error("Gagal memuat data APB Desa"); + return null; } }, async update() { - const cek = templateApbDesa.safeParse(this.form); - if (!cek.success) { - const err = `[${cek.error.issues - .map((v) => `${v.path.join(".")}`) - .join("\n")}] required`; - return toast.error(err); - } - try { this.loading = true; const response = await fetch( @@ -129,34 +124,19 @@ const ApbDesa = proxy({ headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - tahun: this.form.tahun, - pendapatanId: this.form.pendapatanId, - belanjaId: this.form.belanjaId, - pembiayaanId: this.form.pembiayaanId, - }), + body: JSON.stringify(this.form), } ); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.message || `HTTP error! status: ${response.status}` - ); - } - const result = await response.json(); - if (result.success) { - toast.success("Berhasil update APB Desa"); - await ApbDesa.findMany.load(); // refresh list - return true; - } else { - throw new Error(result.message || "Gagal mengupdate APB Desa"); + throw new Error("Gagal memperbarui APB Desa"); } + const data = await response.json(); + toast.success("APB Desa berhasil diperbarui"); + return data; } catch (error) { console.error("Error updating APB Desa:", error); - toast.error( - error instanceof Error ? error.message : "Gagal mengupdate APB Desa" - ); - return false; + toast.error("Gagal memperbarui APB Desa"); + throw error; } finally { this.loading = false; } @@ -169,55 +149,53 @@ const ApbDesa = proxy({ delete: { loading: false, async byId(id: string) { - if (!id) return toast.warn("ID tidak valid"); - try { - ApbDesa.delete.loading = true; - + this.loading = true; const response = await fetch( - `/api/ekonomi/pendapatanaslidesa/apbdesa/del/${id}`, + `/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`, { method: "DELETE", - headers: { - "Content-Type": "application/json", - }, } ); - - const result = await response.json(); - - if (response.ok && result?.success) { - toast.success(result.message || "APB Desa berhasil dihapus"); - await ApbDesa.findMany.load(); // refresh list - } else { - toast.error(result?.message || "Gagal menghapus APB Desa"); + if (!response.ok) { + throw new Error("Gagal menghapus APB Desa"); } + toast.success("APB Desa berhasil dihapus"); + return true; } catch (error) { - console.error("Gagal delete:", error); - toast.error("Terjadi kesalahan saat menghapus APB Desa"); + console.error("Error deleting APB Desa:", error); + toast.error("Gagal menghapus APB Desa"); + return false; } finally { - ApbDesa.delete.loading = false; + this.loading = false; } }, }, findUnique: { - data: null as - | (Prisma.ApbDesaGetPayload<{ - include: { pendapatan: true; belanja: true; pembiayaan: true }; - }> & { isActive: boolean }) - | null, + data: null as Prisma.ApbDesaGetPayload<{ + include: { pendapatan: true; belanja: true; pembiayaan: true }; + }> | null, + async load(id: string) { - const res = await fetch(`/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`); - if (res.ok) { - const json = await res.json(); - ApbDesa.findUnique.data = json.data - ? { - ...json.data, - isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data - } - : null; - } else { - ApbDesa.findUnique.data = null; + try { + const response = await fetch( + `/api/ekonomi/pendapatanaslidesa/apbdesa/${id}` + ); + if (!response.ok) { + throw new Error("Gagal mengambil detail APB Desa"); + } + const result = await response.json(); + + if (!result.success) { + throw new Error(result.message || "Gagal mengambil data"); + } + + this.data = result.data; // ✅ fix utama di sini + return result.data; + } catch (error) { + console.error("Error loading APB Desa detail:", error); + toast.error("Gagal memuat detail APB Desa"); + return null; } }, }, @@ -533,28 +511,38 @@ const belanja = proxy({ return null; } try { - const response = await fetch( - `/api/ekonomi/pendapatanaslidesa/belanja/${id}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ); + const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); if (!response.ok) { - throw new Error("Gagal mengambil Belanja"); + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + if (result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + name: data.name, + value: data.value, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); } - const data = await response.json(); - this.id = id; - this.form = data; } catch (error) { - console.error("Load error:", error); - toast.error("Gagal mengambil Belanja"); + console.error("Error loading belanja:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; } }, + async update() { - const cek = templateBelanja.safeParse(this.form); + const cek = templateBelanja.safeParse(belanja.update.form); if (!cek.success) { const err = `[${cek.error.issues .map((v) => `${v.path.join(".")}`) @@ -563,20 +551,17 @@ const belanja = proxy({ } try { - this.loading = true; - const response = await fetch( - `/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - value: this.form.value, - }), - } - ); + belanja.update.loading = true; + const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + value: this.form.value, + }), + }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( @@ -585,25 +570,25 @@ const belanja = proxy({ } const result = await response.json(); if (result.success) { - toast.success("Berhasil update Belanja"); + toast.success("Berhasil update belanja"); await belanja.findMany.load(); // refresh list return true; } else { - throw new Error(result.message || "Gagal mengupdate Belanja"); + throw new Error(result.message || "Gagal mengupdate belanja"); } } catch (error) { - console.error("Error updating Belanja:", error); + console.error("Error updating belanja:", error); toast.error( - error instanceof Error ? error.message : "Gagal mengupdate Belanja" + error instanceof Error ? error.message : "Gagal mengupdate belanja" ); return false; } finally { - this.loading = false; + belanja.update.loading = false; } }, reset() { - this.id = ""; - this.form = { ...BelanjaDefaultForm }; + belanja.update.id = ""; + belanja.update.form = { ...BelanjaDefaultForm }; }, }, delete: { @@ -741,28 +726,38 @@ const pembiayaan = proxy({ return null; } try { - const response = await fetch( - `/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - } - ); + const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); if (!response.ok) { - throw new Error("Gagal mengambil Pembiayaan"); + throw new Error(`HTTP error! status: ${response.status}`); + } + const result = await response.json(); + if (result?.success) { + const data = result.data; + this.id = data.id; + this.form = { + name: data.name, + value: data.value, + }; + return data; + } else { + throw new Error(result?.message || "Gagal memuat data"); } - const data = await response.json(); - this.id = id; - this.form = data; } catch (error) { - console.error("Load error:", error); - toast.error("Gagal mengambil Pembiayaan"); + console.error("Error loading pembiayaan:", error); + toast.error( + error instanceof Error ? error.message : "Gagal memuat data" + ); + return null; } }, + async update() { - const cek = templatePembiayaan.safeParse(this.form); + const cek = templatePembiayaan.safeParse(pembiayaan.update.form); if (!cek.success) { const err = `[${cek.error.issues .map((v) => `${v.path.join(".")}`) @@ -771,20 +766,17 @@ const pembiayaan = proxy({ } try { - this.loading = true; - const response = await fetch( - `/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: this.form.name, - value: this.form.value, - }), - } - ); + pembiayaan.update.loading = true; + const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.form.name, + value: this.form.value, + }), + }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( @@ -793,25 +785,25 @@ const pembiayaan = proxy({ } const result = await response.json(); if (result.success) { - toast.success("Berhasil update Pembiayaan"); + toast.success("Berhasil update pembiayaan"); await pembiayaan.findMany.load(); // refresh list return true; } else { - throw new Error(result.message || "Gagal mengupdate Pembiayaan"); + throw new Error(result.message || "Gagal mengupdate pembiayaan"); } } catch (error) { - console.error("Error updating Pembiayaan:", error); + console.error("Error updating pembiayaan:", error); toast.error( - error instanceof Error ? error.message : "Gagal mengupdate Pembiayaan" + error instanceof Error ? error.message : "Gagal mengupdate pembiayaan" ); return false; } finally { - this.loading = false; + pembiayaan.update.loading = false; } }, reset() { - this.id = ""; - this.form = { ...PembiayaanDefaultForm }; + pembiayaan.update.id = ""; + pembiayaan.update.form = { ...PembiayaanDefaultForm }; }, }, delete: { diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx index c20bf497..6358cb46 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/_lib/layoutTabs.tsx @@ -9,25 +9,25 @@ function LayoutTabs({ children }: { children: React.ReactNode }) { const router = useRouter() const pathname = usePathname() const tabs = [ - { - label: "Pendapatan", - value: "pendapatan", - href: "/admin/ekonomi/padesa-pendapatan-asli-desa/pendapatan" - }, { label: "APB Desa", value: "apbdesa", - href: "/admin/ekonomi/padesa-pendapatan-asli-desa/apbdesa" + href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa" + }, + { + label: "Pendapatan", + value: "pendapatan", + href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan" }, { label: "Belanja", value: "belanja", - href: "/admin/ekonomi/padesa-pendapatan-asli-desa/belanja" + href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja" }, { label: "Pembiayaan", value: "pembiayaan", - href: "/admin/ekonomi/padesa-pendapatan-asli-desa/pembiayaan" + href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan" }, ]; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx new file mode 100644 index 00000000..69da2f21 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/edit/page.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function Page() { + return ( +
+ Page +
+ ); +} + +export default Page; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx new file mode 100644 index 00000000..c9cd4336 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/[id]/page.tsx @@ -0,0 +1,152 @@ +'use client' +import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import colors from '@/con/colors'; +import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; + + + +function DetailAPBDesa() { + const apbState = useProxy(PendapatanAsliDesa.ApbDesa) + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + const params = useParams() + const router = useRouter() + + useShallowEffect(() => { + console.log("PARAM ID:", params?.id) + apbState.findUnique.load(params?.id as string) + }, []) + + const formatRupiah = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + + const handleHapus = () => { + if (selectedId) { + apbState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa") + } + } + + if (!apbState.findUnique.data) { + return ( + + + + ) + } + + return ( + + + + + + + Detail APB Desa + {apbState.findUnique.data ? ( + + + + Tahun + {apbState.findUnique.data?.tahun} + + + + Detail Pembiayaan + {(apbState.findUnique.data?.pembiayaan || []).map((item) => ( + + {item.name}: {formatRupiah(Number(item.value))} + + ))} + + Total: {formatRupiah((apbState.findUnique.data?.pembiayaan || []) + .reduce((sum, item) => sum + Number(item.value), 0))} + + + + + + Detail Belanja + {(apbState.findUnique.data?.belanja || []).map((item) => ( + + {item.name}: {formatRupiah(Number(item.value))} + + ))} + + Total: {formatRupiah((apbState.findUnique.data?.belanja || []) + .reduce((sum, item) => sum + Number(item.value), 0))} + + + + + + Detail Pendapatan + {(apbState.findUnique.data?.pendapatan || []).map((item) => ( + + {item.name}: {formatRupiah(Number(item.value))} + + ))} + + Total: {formatRupiah((apbState.findUnique.data?.pendapatan || []) + .reduce((sum, item) => sum + Number(item.value), 0))} + + + + + + + + + + ) : null} + + + + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleHapus} + text='Apakah anda yakin ingin menghapus APB Desa ini?' + /> + + ); +} + +export default DetailAPBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx new file mode 100644 index 00000000..3ee52586 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create/page.tsx @@ -0,0 +1,190 @@ +'use client' +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import colors from '@/con/colors'; +import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function CreateAPBDesa() { + const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa) + const router = useRouter() + + const resetForm = () => { + apbDesaState.create.form = { + tahun: 0, + pendapatanIds: [], + belanjaIds: [], + pembiayaanIds: [], + } + } + + const handleSubmit = async () => { + await apbDesaState.create.submit() + resetForm() + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa") + } + + return ( + + + + + + + Create APB Desa + { + apbDesaState.create.form.tahun = Number(val.target.value); + }} + label={Tahun} + placeholder="masukkan tahun" + /> + { + apbDesaState.create.form.pendapatanIds = ids; + }} + /> + { + apbDesaState.create.form.belanjaIds = ids; + }} + /> + { + apbDesaState.create.form.pembiayaanIds = ids; + }} + /> + + + + + ); + +/* Select Pendapatan */ + interface SelectPendapatanProps { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + } + + function SelectPendapatan({ + selectedIds = [], + onSelectionChange, + }: SelectPendapatanProps) { + const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan); + + useShallowEffect(() => { + pendapatanState.findMany.load().then(() => { + console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data); + }); + }, []); + + if (!pendapatanState.findMany.data) { + return ; + } + + return ( + Pendapatan} + data={pendapatanState.findMany.data.map(p => ({ + value: p.id, + label: p.name + }))} + value={selectedIds} + onChange={onSelectionChange} + searchable + clearable + placeholder="Pilih pendapatan..." + nothingFoundMessage="Tidak ditemukan" + /> + ); + } + + /* Select Belanja */ + interface SelectBelanjaProps { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + } + + function SelectBelanja({ + selectedIds = [], + onSelectionChange, + }: SelectBelanjaProps) { + const belanjaState = useProxy(PendapatanAsliDesa.belanja); + + useShallowEffect(() => { + belanjaState.findMany.load().then(() => { + console.log("Belanja berhasil dimuat:", belanjaState.findMany.data); + }); + }, []); + + if (!belanjaState.findMany.data) { + return ; + } + + return ( + Belanja} + data={belanjaState.findMany.data.map(b => ({ + value: b.id, + label: b.name + }))} + value={selectedIds} + onChange={onSelectionChange} + searchable + clearable + placeholder="Pilih belanja..." + nothingFoundMessage="Tidak ditemukan" + /> + ); + } + + /* Select Pembiayaan */ + interface SelectPembiayaanProps { + selectedIds: string[]; + onSelectionChange: (ids: string[]) => void; + } + + function SelectPembiayaan({ + selectedIds = [], + onSelectionChange, + }: SelectPembiayaanProps) { + const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); + + useShallowEffect(() => { + pembiayaanState.findMany.load().then(() => { + console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data); + }); + }, []); + + if (!pembiayaanState.findMany.data) { + return ; + } + + return ( + Pembiayaan} + data={pembiayaanState.findMany.data.map(b => ({ + value: b.id, + label: b.name + }))} + value={selectedIds} + onChange={onSelectionChange} + searchable + clearable + placeholder="Pilih pembiayaan..." + nothingFoundMessage="Tidak ditemukan" + /> + ); + } +} + +export default CreateAPBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx new file mode 100644 index 00000000..11b7e353 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/page.tsx @@ -0,0 +1,106 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; + + +function APBDesa() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListAPBDesa({ search }: { search: string }) { + const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa) + const router = useRouter(); + + const formatRupiah = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + + useShallowEffect(() => { + apbDesaState.findMany.load(); + }, []) + + const filteredData = (apbDesaState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.tahun.toString().toLowerCase().includes(keyword) || + item.pembiayaan.map((item) => item.value.toString()).includes(keyword) || + item.belanja.map((item) => item.value.toString()).includes(keyword) || + item.pendapatan.map((item) => item.value.toString()).includes(keyword) + ); + }); + + if (!apbDesaState.findMany.data) { + return ( + + + + ) + } + return ( + + + + + + + Tahun + Pembiayaan + Belanja + Pendapatan + Detail + + + + {filteredData.map((item) => ( + + {item.tahun} + {formatRupiah(item.pembiayaan.reduce((sum, item) => sum + Number(item.value), 0))} + {formatRupiah(item.belanja.reduce((sum, item) => sum + Number(item.value), 0))} + {formatRupiah(item.pendapatan.reduce((sum, item) => sum + Number(item.value), 0))} + + + + + ))} + +
+
+
+ ); +} + +export default APBDesa; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx new file mode 100644 index 00000000..33bca9e2 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/[id]/page.tsx @@ -0,0 +1,112 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditBelanja() { + const belanjaState = useProxy(PendapatanAsliDesa.belanja); + const router = useRouter(); + const params = useParams(); + + const [formData, setFormData] = useState({ + name: belanjaState.update.form.name || '', + value: belanjaState.update.form.value || '', + }); + + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + + useEffect(() => { + const loadBelanja = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await belanjaState.update.load(id); + if (data) { + setFormData({ + name: data.name || '', + value: data.value || '', + }); + } + } catch (error) { + console.error("Error loading belanja:", error); + toast.error("Gagal memuat data belanja"); + } + }; + + loadBelanja(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + belanjaState.update.form = { + ...belanjaState.update.form, + name: formData.name, + value: Number(formData.value), + } + + await belanjaState.update.update(); + toast.success("Jenis Belanja berhasil diperbarui!"); + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja"); + } catch (error) { + console.error("Error updating jenis belanja:", error); + toast.error("Terjadi kesalahan saat memperbarui jenis belanja"); + } + } + + return ( + + + + + + + + Edit Jenis Pendapatan + { + setFormData({ ...formData, name: val.target.value }); + }} + label={Nama Jenis Pendapatan} + placeholder='Masukkan nama Jenis Pendapatan' + /> + Nilai} + placeholder='Masukkan nilai' + value={formatRupiah(formData.value)} + onChange={(val) => { + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + setFormData({ ...formData, value: cleanValue }); + }} + /> + + + + + + + ); +} + +export default EditBelanja; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx new file mode 100644 index 00000000..c03cb35c --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/create/page.tsx @@ -0,0 +1,77 @@ +'use client' +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useProxy } from 'valtio/utils'; + +function CreateBelanja() { + const belanjaState = useProxy(PendapatanAsliDesa.belanja) + const router = useRouter() + + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + + const resetForm = () => { + belanjaState.create.form = { + name: "", + value: 0, + } + } + + const handleSubmit = async () => { + await belanjaState.create.submit(); + resetForm() + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja") + } + return ( + + + + + + + + Create Jenis Belanja + { + belanjaState.create.form.name = val.target.value; + }} + label={Nama Jenis Belanja} + placeholder='Masukkan nama jenis belanja' + /> + { + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + belanjaState.create.form.value = cleanValue; + }} + label={Nilai} + placeholder='Masukkan nilai' + /> + + + + + + + ); +} + +export default CreateBelanja; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx new file mode 100644 index 00000000..d63f0ce7 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/belanja/page.tsx @@ -0,0 +1,139 @@ +'use client' +import colors from '@/con/colors'; +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import { useShallowEffect } from '@mantine/hooks'; +import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import { useProxy } from 'valtio/utils'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; +import HeaderSearch from '../../../_com/header'; +import JudulList from '../../../_com/judulList'; + + +function Belanja() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListBelanja({ search }: { search: string }) { + const belanjaState = useProxy(PendapatanAsliDesa.belanja) + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + + const formatRupiah = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + + const totalBelanja = belanjaState.findMany.data.reduce((sum, item) => sum + item.value, 0); + + const handleDelete = () => { + if (selectedId) { + belanjaState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + belanjaState.findMany.load() + } + } + + useShallowEffect(() => { + belanjaState.findMany.load(); + }, []) + + const filteredData = (belanjaState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.name.toLowerCase().includes(keyword) || + item.value.toString().toLowerCase().includes(keyword) + ); + }); + + if (!belanjaState.findMany.data) { + return ( + + + + ) + } + return ( + + + + + + + Nama + Nilai + Persentase + Edit + Delete + + + + {filteredData.map((item) => ( + + {item.name} + {formatRupiah(item.value)} + {((item.value / totalBelanja) * 100).toFixed(0)}% + + + + + + + + ))} + + + Total + + + {formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))} + + + +
+
+ + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleDelete} + text='Apakah anda yakin ingin menghapus belanja ini?' + /> +
+ ); +} + +export default Belanja; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx new file mode 100644 index 00000000..79ef3847 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/[id]/page.tsx @@ -0,0 +1,112 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +'use client' +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import colors from '@/con/colors'; +import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; +import { useProxy } from 'valtio/utils'; + +function EditPembiayaan() { + const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan); + const router = useRouter(); + const params = useParams(); + + const [formData, setFormData] = useState({ + name: pembiayaanState.update.form.name || '', + value: pembiayaanState.update.form.value || '', + }); + + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + + useEffect(() => { + const loadPembiayaan = async () => { + const id = params?.id as string; + if (!id) return; + + try { + const data = await pembiayaanState.update.load(id); + if (data) { + setFormData({ + name: data.name || '', + value: data.value || '', + }); + } + } catch (error) { + console.error("Error loading pembiayaan:", error); + toast.error("Gagal memuat data pembiayaan"); + } + }; + + loadPembiayaan(); + }, [params?.id]); + + const handleSubmit = async () => { + try { + pembiayaanState.update.form = { + ...pembiayaanState.update.form, + name: formData.name, + value: Number(formData.value), + } + + await pembiayaanState.update.update(); + toast.success("Jenis Pembiayaan berhasil diperbarui!"); + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan"); + } catch (error) { + console.error("Error updating jenis pembiayaan:", error); + toast.error("Terjadi kesalahan saat memperbarui jenis pembiayaan"); + } + } + + return ( + + + + + + + + Edit Jenis Pembiayaan + { + setFormData({ ...formData, name: val.target.value }); + }} + label={Nama Jenis Pembiayaan} + placeholder='Masukkan nama Jenis Pembiayaan' + /> + Nilai} + placeholder='Masukkan nilai' + value={formatRupiah(formData.value)} + onChange={(val) => { + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + setFormData({ ...formData, value: cleanValue }); + }} + /> + + + + + + + ); +} + +export default EditPembiayaan; \ No newline at end of file diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx new file mode 100644 index 00000000..4a81d990 --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create/page.tsx @@ -0,0 +1,78 @@ +'use client' +import React from 'react'; +import { useProxy } from 'valtio/utils'; +import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'; +import { useRouter } from 'next/navigation'; +import colors from '@/con/colors'; +import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core'; +import { IconArrowBack } from '@tabler/icons-react'; + +function CreatePembiayaan() { + const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan) + const router = useRouter() + + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + + const resetForm = () => { + pembiayaanState.create.form = { + name: "", + value: 0, + } + } + + const handleSubmit = async () => { + await pembiayaanState.create.submit(); + resetForm() + router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan") + } + return ( + + + + + + + + Create Jenis Pembiayaan + { + pembiayaanState.create.form.name = val.target.value; + }} + label={Nama Jenis Pembiayaan} + placeholder='Masukkan nama jenis pembiayaan' + /> + { + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + pembiayaanState.create.form.value = cleanValue; + }} + label={Nilai} + placeholder='Masukkan nilai' + /> + + + + + + + ); +} + +export default CreatePembiayaan; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx new file mode 100644 index 00000000..4f31cafc --- /dev/null +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/page.tsx @@ -0,0 +1,138 @@ +'use client' +import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; +import React, { useState } from 'react'; +import HeaderSearch from '../../../_com/header'; +import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; +import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa'; +import { useProxy } from 'valtio/utils'; +import { useRouter } from 'next/navigation'; +import { useShallowEffect } from '@mantine/hooks'; +import colors from '@/con/colors'; +import JudulList from '../../../_com/judulList'; +import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; + +function Pembiayaan() { + const [search, setSearch] = useState(""); + return ( + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + + ); +} + +function ListPembiayaan({ search }: { search: string }) { + const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan) + const router = useRouter(); + const [modalHapus, setModalHapus] = useState(false) + const [selectedId, setSelectedId] = useState(null) + + const formatRupiah = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + + const totalPembiayaan = pembiayaanState.findMany.data.reduce((sum, item) => sum + item.value, 0); + + const handleDelete = () => { + if (selectedId) { + pembiayaanState.delete.byId(selectedId) + setModalHapus(false) + setSelectedId(null) + pembiayaanState.findMany.load() + } + } + + useShallowEffect(() => { + pembiayaanState.findMany.load(); + }, []) + + const filteredData = (pembiayaanState.findMany.data || []).filter(item => { + const keyword = search.toLowerCase(); + return ( + item.name.toLowerCase().includes(keyword) || + item.value.toString().toLowerCase().includes(keyword) + ); + }); + + if (!pembiayaanState.findMany.data) { + return ( + + + + ) + } + return ( + + + + + + + Nama + Nilai + Persentase + Edit + Delete + + + + {filteredData.map((item) => ( + + {item.name} + {formatRupiah(item.value)} + {((item.value / totalPembiayaan) * 100).toFixed(0)}% + + + + + + + + ))} + + + Total + + + {formatRupiah(pembiayaanState.findMany.data.reduce((total, item) => total + item.value, 0))} + + + +
+
+ + {/* Modal Konfirmasi Hapus */} + setModalHapus(false)} + onConfirm={handleDelete} + text='Apakah anda yakin ingin menghapus pembiayaan ini?' + /> +
+ ) +} + +export default Pembiayaan; diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx index 4c30ab39..2ffc7285 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/[id]/page.tsx @@ -19,6 +19,19 @@ function EditPendapatan() { value: pendapatanState.update.form.value || '', }); + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + useEffect(() => { const loadPendapatan = async () => { const id = params?.id as string; @@ -80,9 +93,11 @@ function EditPendapatan() { Nilai} placeholder='Masukkan nilai' - value={formData.value} + value={formatRupiah(formData.value)} onChange={(val) => { - setFormData({ ...formData, value: val.target.value }); + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + setFormData({ ...formData, value: cleanValue }); }} /> diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx index 9c7b9040..fdf9a7b0 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create/page.tsx @@ -10,6 +10,19 @@ function CreatePendapatan() { const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan) const router = useRouter() + const formatRupiah = (value: number | string) => { + const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, '')); + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(number); + }; + + const unformatRupiah = (value: string) => { + return Number(value.replace(/\D/g, '')); + }; + const resetForm = () => { pendapatanState.create.form = { name: "", @@ -42,10 +55,12 @@ function CreatePendapatan() { placeholder='Masukkan nama jenis pendapatan' /> { - pendapatanState.create.form.value = Number(val.target.value); + const raw = val.currentTarget.value; + const cleanValue = unformatRupiah(raw); + pendapatanState.create.form.value = cleanValue; }} label={Nilai} placeholder='Masukkan nilai' diff --git a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx index 7a19003b..0bde96df 100644 --- a/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx +++ b/src/app/admin/(dashboard)/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/page.tsx @@ -34,6 +34,14 @@ function ListPendapatan({ search }: { search: string }) { const [modalHapus, setModalHapus] = useState(false) const [selectedId, setSelectedId] = useState(null) + const formatRupiah = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); + }; + const handleDelete = () => { if (selectedId) { @@ -83,7 +91,7 @@ function ListPendapatan({ search }: { search: string }) { {filteredData.map((item) => ( {item.name} - {item.value} + {formatRupiah(item.value)}