Compare commits

...

6 Commits

61 changed files with 2151 additions and 1456 deletions

View File

@@ -292,6 +292,9 @@ model PosisiOrganisasiPPID {
pegawai PegawaiPPID[] pegawai PegawaiPPID[]
strukturOrganisasi StrukturPPID[] // Relasi balik strukturOrganisasi StrukturPPID[] // Relasi balik
parentId String? parentId String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id]) parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
children PosisiOrganisasiPPID[] @relation("Parent") children PosisiOrganisasiPPID[] @relation("Parent")
} }
@@ -1547,7 +1550,7 @@ model DataDemografiPekerjaan {
model DetailDataPengangguran { model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20) month String @db.VarChar(20)
year Int year DateTime
totalUnemployment Int totalUnemployment Int
educatedUnemployment Int educatedUnemployment Int
uneducatedUnemployment Int uneducatedUnemployment Int

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -11,8 +12,7 @@ const templateForm = z.object({
statistik: z.object({ statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"), tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"), jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
}) }),
}); });
const defaultForm = { const defaultForm = {
@@ -21,8 +21,8 @@ const defaultForm = {
ikonUrl: "", ikonUrl: "",
statistik: { statistik: {
tahun: "", tahun: "",
jumlah: "" jumlah: "",
} },
}; };
const programKemiskinanState = proxy({ const programKemiskinanState = proxy({
@@ -64,12 +64,35 @@ const programKemiskinanState = proxy({
}; };
}>[], }>[],
loading: false, loading: false,
async load() { page: 1,
const res = await ApiFetch.api.ekonomi.programkemiskinan[ totalPages: 1,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
programKemiskinanState.findMany.data = res.data?.data ?? []; programKemiskinanState.findMany.page = page;
programKemiskinanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKemiskinanState.findMany.data = res.data.data ?? [];
programKemiskinanState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch program kemiskinan paginated:", err);
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
} finally {
programKemiskinanState.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -50,18 +51,50 @@ const apbdes = proxy({
}, },
}, },
findMany: { findMany: {
data: null as Array< data: null as
Prisma.APBDesGetPayload<{ | Prisma.APBDesGetPayload<{
include: { include: {
image: true; image: true;
file: true; file: true;
}; };
}> }>[]
> | null, | null,
async load() { page: 1,
const res = await ApiFetch.api.landingpage.apbdes["find-many"].get(); totalPages: 1,
if (res.status === 200) { total: 0,
apbdes.findMany.data = res.data?.data ?? []; loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
apbdes.findMany.loading = true; // Use the full path to access the property
apbdes.findMany.page = page;
apbdes.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.apbdes[
"findMany"
].get({
query
});
if (res.status === 200 && res.data?.success) {
apbdes.findMany.data = res.data.data || [];
apbdes.findMany.total = res.data.total || 0;
apbdes.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
apbdes.findMany.data = [];
apbdes.findMany.total = 0;
apbdes.findMany.totalPages = 1;
} finally {
apbdes.findMany.loading = false;
} }
}, },
}, },

View File

@@ -60,16 +60,22 @@ const desaAntikorupsi = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
desaAntikorupsi.findMany.loading = true; // Use the full path to access the property
desaAntikorupsi.findMany.page = page; desaAntikorupsi.findMany.page = page;
desaAntikorupsi.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.desaantikorupsi[ const res = await ApiFetch.api.landingpage.desaantikorupsi[
"findMany" "findMany"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
desaAntikorupsi.findMany.data = res.data.data || []; desaAntikorupsi.findMany.data = res.data.data || [];
desaAntikorupsi.findMany.total = res.data.total || 0; desaAntikorupsi.findMany.total = res.data.total || 0;
@@ -305,20 +311,25 @@ const kategoriDesaAntiKorupsi = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
kategoriDesaAntiKorupsi.findMany.loading = true; // Use the full path to access the property
kategoriDesaAntiKorupsi.findMany.page = page; kategoriDesaAntiKorupsi.findMany.page = page;
kategoriDesaAntiKorupsi.findMany.search = search;
try { try {
const res = await ApiFetch.api.landingpage.kategoridak[ const query: any = { page, limit };
"findMany" if (search) query.search = search;
].get({
query: { page, limit }, const res = await ApiFetch.api.landingpage.kategoridak["findMany"].get({
query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
kategoriDesaAntiKorupsi.findMany.data = res.data.data || []; kategoriDesaAntiKorupsi.findMany.data = res.data.data || [];
kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0; kategoriDesaAntiKorupsi.findMany.total = res.data.total || 0;
kategoriDesaAntiKorupsi.findMany.totalPages = res.data.totalPages || 1; kategoriDesaAntiKorupsi.findMany.totalPages =
res.data.totalPages || 1;
} else { } else {
console.error("Failed to load media sosial:", res.data?.message); console.error("Failed to load media sosial:", res.data?.message);
kategoriDesaAntiKorupsi.findMany.data = []; kategoriDesaAntiKorupsi.findMany.data = [];
@@ -363,27 +374,30 @@ const kategoriDesaAntiKorupsi = proxy({
try { try {
kategoriDesaAntiKorupsi.delete.loading = true; kategoriDesaAntiKorupsi.delete.loading = true;
const response = await fetch( const response = await fetch(`/api/landingpage/kategoridak/del/${id}`, {
`/api/landingpage/kategoridak/del/${id}`, method: "DELETE",
{ headers: {
method: "DELETE", "Content-Type": "application/json",
headers: { },
"Content-Type": "application/json", });
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok && result?.success) { if (response.ok && result?.success) {
toast.success(result.message || "Kategori desa anti korupsi berhasil dihapus"); toast.success(
result.message || "Kategori desa anti korupsi berhasil dihapus"
);
await kategoriDesaAntiKorupsi.findMany.load(); // refresh list await kategoriDesaAntiKorupsi.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus kategori desa anti korupsi"); toast.error(
result?.message || "Gagal menghapus kategori desa anti korupsi"
);
} }
} catch (error) { } catch (error) {
console.error("Gagal delete:", error); console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kategori desa anti korupsi"); toast.error(
"Terjadi kesalahan saat menghapus kategori desa anti korupsi"
);
} finally { } finally {
kategoriDesaAntiKorupsi.delete.loading = false; kategoriDesaAntiKorupsi.delete.loading = false;
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -62,12 +63,34 @@ const prestasiDesa = proxy({
}; };
}> }>
> | null, > | null,
async load() { page: 1,
const res = await ApiFetch.api.landingpage.prestasidesa[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
prestasiDesa.findMany.data = res.data?.data ?? []; prestasiDesa.findMany.loading = true; // ✅ Akses langsung via nama path
prestasiDesa.findMany.page = page;
prestasiDesa.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.prestasidesa["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
prestasiDesa.findMany.data = res.data.data ?? [];
prestasiDesa.findMany.totalPages = res.data.totalPages ?? 1;
} else {
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch prestasi desa paginated:", err);
prestasiDesa.findMany.data = [];
prestasiDesa.findMany.totalPages = 1;
} finally {
prestasiDesa.findMany.loading = false;
} }
}, },
}, },
@@ -283,12 +306,34 @@ const kategoriPrestasi = proxy({
id: string; id: string;
name: string; name: string;
}> | null, }> | null,
async load() { page: 1,
const res = await ApiFetch.api.landingpage.kategoriprestasi[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
kategoriPrestasi.findMany.data = res.data?.data ?? []; kategoriPrestasi.findMany.loading = true; // ✅ Akses langsung via nama path
kategoriPrestasi.findMany.page = page;
kategoriPrestasi.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.kategoriprestasi["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
kategoriPrestasi.findMany.data = res.data.data ?? [];
kategoriPrestasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
kategoriPrestasi.findMany.data = [];
kategoriPrestasi.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch kategori prestasi paginated:", err);
kategoriPrestasi.findMany.data = [];
kategoriPrestasi.findMany.totalPages = 1;
} finally {
kategoriPrestasi.findMany.loading = false;
} }
}, },
}, },

View File

@@ -65,14 +65,19 @@ const programInovasi = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
programInovasi.findMany.loading = true; // Use the full path to access the property programInovasi.findMany.loading = true; // Use the full path to access the property
programInovasi.findMany.page = page; programInovasi.findMany.page = page;
programInovasi.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.programinovasi[ const res = await ApiFetch.api.landingpage.programinovasi[
"findMany" "findMany"
].get({ ].get({
query: { page, limit }, query
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
@@ -482,14 +487,19 @@ const mediaSosial = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
mediaSosial.findMany.loading = true; // Use the full path to access the property mediaSosial.findMany.loading = true; // Use the full path to access the property
mediaSosial.findMany.page = page; mediaSosial.findMany.page = page;
try { mediaSosial.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.mediasosial[ const res = await ApiFetch.api.landingpage.mediasosial[
"findMany" "findMany"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -58,14 +58,19 @@ const sdgsDesa = proxy({
totalPages: 1, totalPages: 1,
total: 0, total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function search: "",
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
sdgsDesa.findMany.loading = true; // Use the full path to access the property sdgsDesa.findMany.loading = true; // Use the full path to access the property
sdgsDesa.findMany.page = page; sdgsDesa.findMany.page = page;
sdgsDesa.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.landingpage.sdgsdesa[ const res = await ApiFetch.api.landingpage.sdgsdesa[
"findMany" "findMany"
].get({ ].get({
query: { page, limit }, query,
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {

View File

@@ -348,18 +348,34 @@ const posisiOrganisasi = proxy({
deskripsi: string | null; deskripsi: string | null;
hierarki: number; hierarki: number;
}>, }>,
async load() { page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
posisiOrganisasi.findMany.page = page;
posisiOrganisasi.findMany.search = search;
try { try {
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[ const query: any = { page, limit };
"find-many" if (search) query.search = search;
].get();
if (res.status === 200) { const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? []; if (res.status === 200 && res.data?.success) {
posisiOrganisasi.findMany.data = res.data.data ?? [];
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
} else {
posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Find many error:", error); console.error("Gagal fetch posisi organisasi paginated:", err);
this.data = []; posisiOrganisasi.findMany.data = [];
posisiOrganisasi.findMany.totalPages = 1;
} finally {
posisiOrganisasi.findMany.loading = false;
} }
}, },
}, },
@@ -438,9 +454,9 @@ const pegawai = proxy({
try { try {
pegawai.create.loading = true; pegawai.create.loading = true;
const res = await ApiFetch.api.ppid.strukturppid.pegawai[ const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
"create" pegawai.create.form
].post(pegawai.create.form); );
if (res.status === 200) { if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan"); toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load(); await pegawai.findMany.load();
@@ -457,42 +473,55 @@ const pegawai = proxy({
}, },
// In struktur-organisasi.ts // In struktur-organisasi.ts
findMany: { findMany: {
data: null as any[] | null, data: null as
page: 1, | Prisma.PegawaiPPIDGetPayload<{
totalPages: 1, include: {
total: 0, image: true;
loading: false, posisi: true;
load: async (page = 1, limit = 10) => { // Change to arrow function };
pegawai.findMany.loading = true; // Use the full path to access the property }>[]
pegawai.findMany.page = page; | null,
try { page: 1,
const res = await ApiFetch.api.ppid.strukturppid.pegawai[ totalPages: 1,
"find-many" total: 0,
].get({ loading: false,
query: { page, limit }, search: "",
}); load: async (page = 1, limit = 10, search = "") => {
// Change to arrow function
pegawai.findMany.loading = true; // Use the full path to access the property
pegawai.findMany.page = page;
pegawai.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
if (res.status === 200 && res.data?.success) { const res = await ApiFetch.api.ppid.strukturppid.pegawai[
pegawai.findMany.data = res.data.data || []; "find-many"
pegawai.findMany.total = res.data.total || 0; ].get({
pegawai.findMany.totalPages = res.data.totalPages || 1; query,
} else { });
console.error("Failed to load pegawai:", res.data?.message);
if (res.status === 200 && res.data?.success) {
pegawai.findMany.data = res.data.data || [];
pegawai.findMany.total = res.data.total || 0;
pegawai.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pegawai:", res.data?.message);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pegawai:", error);
pegawai.findMany.data = []; pegawai.findMany.data = [];
pegawai.findMany.total = 0; pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1; pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
} }
} catch (error) { },
console.error("Error loading pegawai:", error);
pegawai.findMany.data = [];
pegawai.findMany.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
}, },
},
findUnique: { findUnique: {
data: null as data: null as
| (Prisma.PegawaiGetPayload<{ | (Prisma.PegawaiGetPayload<{
@@ -521,12 +550,9 @@ findMany: {
if (!id) return toast.warn("ID tidak valid"); if (!id) return toast.warn("ID tidak valid");
try { try {
pegawai.delete.loading = true; pegawai.delete.loading = true;
const res = await fetch( const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
`/api/ppid/strukturppid/pegawai/del/${id}`, method: "DELETE",
{ });
method: "DELETE",
}
);
const json = await res.json(); const json = await res.json();
if (res.ok) { if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai"); toast.success(json.message ?? "Berhasil hapus pegawai");
@@ -555,15 +581,12 @@ findMany: {
} }
try { try {
const response = await fetch( const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
`/api/ppid/strukturppid/pegawai/${id}`, method: "GET",
{ headers: {
method: "GET", "Content-Type": "application/json",
headers: { },
"Content-Type": "application/json", });
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
@@ -677,7 +700,7 @@ findMany: {
const stateStrukturPPID = proxy({ const stateStrukturPPID = proxy({
stateStruktur, stateStruktur,
posisiOrganisasi, posisiOrganisasi,
pegawai pegawai,
}); });
export default stateStrukturPPID; export default stateStrukturPPID;

View File

@@ -143,8 +143,8 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
dataKey="pekerjaan" dataKey="pekerjaan"
type="stacked" type="stacked"
series={[ series={[
{ name: 'lakiLaki', color: 'red.6', label: 'Laki - Laki' }, { name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: 'orange.6', label: 'Perempuan' }, { name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]} ]}
/> />
</Box> </Box>

View File

@@ -35,7 +35,7 @@ function EditJumlahPendudukMiskin() {
// Set the ID before submitting // Set the ID before submitting
stateJPM.update.id = id; stateJPM.update.id = id;
await stateJPM.update.submit(); await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin-2024-2025') router.push('/admin/ekonomi/jumlah-penduduk-miskin')
} }
return ( return (
<Box> <Box>

View File

@@ -32,7 +32,7 @@ function CreateJumlahPendudukMiskin() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin-2024-2025"); router.push("/admin/ekonomi/jumlah-penduduk-miskin");
} }
return ( return (
<Box> <Box>

View File

@@ -91,7 +91,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Jumlah Penduduk Miskin' title='List Jumlah Penduduk Miskin'
href='/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/create' href='/admin/ekonomi/jumlah-penduduk-miskin/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -108,7 +108,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
<TableTd>{item.year}</TableTd> <TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin-2024-2025/${item.id}`)}> <Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -8,29 +8,36 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() { function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title> <HeaderSearch
<ListGrafikBerdasarkanPendidikan /> title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanPendidikan search={search}/>
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanPendidikan() { function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState("");
const handleDelete = async () => { const handleDelete = async () => {
@@ -56,11 +63,11 @@ function ListGrafikBerdasarkanPendidikan() {
const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0); const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0); const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
setDonutData([ setDonutData([
{ name: 'SD', value: SD, color: colors['blue-button'], key: 'SD' }, { name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
{ name: 'SMP', value: SMP, color: '#10A85AFF', key: 'SMP' }, { name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#C07B13FF', key: 'SMA' }, { name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
{ name: 'D3', value: D3, color: '#1094A8FF', key: 'D3' }, { name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
{ name: 'S1', value: S1, color: '#A83610FF', key: 'S1' }, { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data])
@@ -88,13 +95,9 @@ function ListGrafikBerdasarkanPendidikan() {
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}> <Paper bg={colors['white-1']} p={"md"}>
<JudulListTab <JudulList
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Grafik Pengangguran Berdasarkan Pendidikan' title='List Grafik Pengangguran Berdasarkan Pendidikan'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create' href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>

View File

@@ -8,29 +8,37 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/judulListTab'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanUsiaKerjaYangMenganggur() { function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title> <HeaderSearch
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur /> title='Detail Data Pengangguran'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur() { function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState("");
const handleDelete = async () => { const handleDelete = async () => {
@@ -85,13 +93,9 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur() {
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}> <Paper bg={colors['white-1']} p={"md"}>
<JudulListTab <JudulList
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
title='List Pengangguran Berdasarkan Usia Kerja' title='List Pengangguran Berdasarkan Usia Kerja'
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create' href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>

View File

@@ -25,7 +25,7 @@ function DetailJumlahPengangguran() {
stateDetail.delete.byId(selectedId) stateDetail.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran") router.push("/admin/ekonomi/jumlah-pengangguran")
} }
} }
@@ -86,7 +86,7 @@ function DetailJumlahPengangguran() {
color={"red"}> color={"red"}>
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green"> <Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Flex>

View File

@@ -90,7 +90,7 @@ function CreateJumlahPengangguran() {
/> />
<TextInput <TextInput
label="Tahun" label="Tahun"
type="number" type="date"
value={stateDetail.create.form.year} value={stateDetail.create.form.year}
onChange={(e) => onChange={(e) =>
(stateDetail.create.form.year = Number(e.currentTarget.value)) (stateDetail.create.form.year = Number(e.currentTarget.value))

View File

@@ -8,21 +8,29 @@ import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
import JudulListTab from '../../_com/judulListTab';
function DetailDataPengangguran() { function DetailDataPengangguran() {
const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<Stack gap={"xs"}> <Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title> <HeaderSearch
<ListDetailDataPengangguran /> title='Detail Data Pengangguran'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDetailDataPengangguran search={search} />
</Stack> </Stack>
</Box> </Box>
); );
} }
function ListDetailDataPengangguran() { function ListDetailDataPengangguran({search}: {search: string}) {
type DetailDataPengangguran = { type DetailDataPengangguran = {
id: string; id: string;
@@ -37,7 +45,6 @@ function ListDetailDataPengangguran() {
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState("")
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true)
@@ -78,13 +85,9 @@ function ListDetailDataPengangguran() {
<Box> <Box>
<Stack gap={"md"}> <Stack gap={"md"}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulListTab <JudulList
title='List Detail Data Pengangguran' title='List Detail Data Pengangguran'
href='/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/create' href='/admin/ekonomi/jumlah-pengangguran/create'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -102,7 +105,7 @@ function ListDetailDataPengangguran() {
<TableTd>{item.educatedUnemployment}</TableTd> <TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd> <TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${item.id}`)}> <Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -23,7 +23,7 @@ function ProgramKemiskinan() {
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListProgramKemiskinan search={search}/> <ListProgramKemiskinan search={search} />
</Box> </Box>
); );
} }
@@ -34,14 +34,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const {
data,
page,
totalPages,
loading,
load,
} = programState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true)
programState.findMany.load() load(page, 10, search)
}, []) }, [])
useEffect(() => { useEffect(() => {
if (programState.findMany.data) { if (data) {
const chartData = programState.findMany.data const chartData = data
.filter(item => item.statistik) .filter(item => item.statistik)
.map(item => ({ .map(item => ({
tahun: item.statistik?.tahun, tahun: item.statistik?.tahun,
@@ -52,18 +60,11 @@ function ListProgramKemiskinan({ search }: { search: string }) {
setLineChart(chartData); setLineChart(chartData);
} }
}, [programState.findMany.data]) }, [data])
const filteredData = (programState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.statistik?.tahun.toString().includes(keyword)
);
});
if (!programState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -112,7 +113,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
<Box > <Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title> <Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}> {mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{overflowX: 'auto'}}> <Box w={"100%"} style={{ overflowX: 'auto' }}>
<LineChart <LineChart
width={820} width={820}
height={300} height={300}
@@ -143,6 +144,14 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -2,15 +2,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
import CreateEditor from '../../../_com/createEditor';
function CreateSektorUnggulanDesa() { function CreateSektorUnggulanDesa() {
const stateGrafik= useProxy(grafikSektorUnggulan); const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter()
@@ -54,15 +55,15 @@ function CreateSektorUnggulanDesa() {
stateGrafik.create.form.name = val.currentTarget.value; stateGrafik.create.form.name = val.currentTarget.value;
}} }}
/> />
<TextInput <Box>
label="Deskripsi Sektor Unggulan" <Text fw={"bold"} fz={"sm"}>Deskripsi Sektor Ungggulan</Text>
type="text" <CreateEditor
value={stateGrafik.create.form.description} value={stateGrafik.create.form.description}
placeholder="Masukkan deskripsi sektor unggulan" onChange={(val) => {
onChange={(val) => { stateGrafik.create.form.description = val;
stateGrafik.create.form.description = val.currentTarget.value; }}
}} />
/> </Box>
<TextInput <TextInput
label="Jumlah" label="Jumlah"
type="number" type="number"

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,7 +30,7 @@ function SektorUnggulanDesa() {
function ListSektorUnggulanDesa({ search }: { search: string }) { function ListSektorUnggulanDesa({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const state = useProxy(grafikSektorUnggulan); const state = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<{id: string; name: string; description: string | null; value: number | null}[]>([]); const [chartData, setChartData] = useState<{ id: string; name: string; description: string | null; value: number | null }[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)') const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)')
@@ -61,38 +61,41 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
}, [state.findMany.data]); }, [state.findMany.data]);
return ( return (
<Box py={10}> <Box>
<Paper bg={colors['white-1']} p={'md'}> <Stack gap={"xs"}>
<JudulList <Paper bg={colors['white-1']} p={'md'}>
title='List Sektor Unggulan Desa' <JudulList
href='/admin/ekonomi/sektor-unggulan-desa/create' title='List Sektor Unggulan Desa'
/> href='/admin/ekonomi/sektor-unggulan-desa/create'
<Table striped withTableBorder withRowBorders> />
<TableThead> <Table striped withTableBorder withRowBorders>
<TableTr> <TableThead>
<TableTh>Nama Sektor Unggulan</TableTh> <TableTr>
<TableTh>Deskripsi Sektor Unggulan</TableTh> <TableTh>Nama Sektor Unggulan</TableTh>
<TableTh>Detail</TableTh> <TableTh>Deskripsi Sektor Unggulan</TableTh>
</TableTr> <TableTh>Detail</TableTh>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.description}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.map((item) => (
</Paper> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate={"end"} fz={'sm'} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '' }}></Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */} {/* Chart */}
{!mounted && !chartData ? ( {!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title> <Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
@@ -115,6 +118,7 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
</Paper> </Paper>
</Box> </Box>
)} )}
</Stack>
</Box> </Box>
); );
} }

View File

@@ -51,7 +51,7 @@ function DetailFasilitasKesehatan() {
<GridCol span={12}> <GridCol span={12}>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text> <Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text>
</GridCol> </GridCol>
<GridCol span={12}> {/* <GridCol span={12}>
<Flex gap={"xs"}> <Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}> <Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}>
Tambah Dokter Tambah Dokter
@@ -60,7 +60,7 @@ function DetailFasilitasKesehatan() {
Tambah Layanan Tambah Layanan
</Button> </Button>
</Flex> </Flex>
</GridCol> </GridCol> */}
</Grid> </Grid>
{stateFasilitasKesehatan.findUnique.data ? ( {stateFasilitasKesehatan.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}> <Paper bg={colors['BG-trans']} p={'md'}>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { ActionIcon, Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconFile, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
@@ -30,19 +30,22 @@ function APBDes() {
function ListAPBDes({ search }: { search: string }) { function ListAPBDes({ search }: { search: string }) {
const listState = useProxy(apbdes) const listState = useProxy(apbdes)
const router = useRouter(); const router = useRouter();
useEffect(() => {
listState.findMany.load()
}, [])
const filteredData = (listState.findMany.data || []).filter(item => { const {
const keyword = search.toLowerCase(); data,
return ( page,
item.name.toLowerCase().includes(keyword) || totalPages,
item.jumlah.toLowerCase().includes(keyword) loading,
) load,
}); } = listState.findMany
if (!listState.findMany.data) { useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || []
if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -88,7 +91,7 @@ function ListAPBDes({ search }: { search: string }) {
rel="noopener noreferrer" rel="noopener noreferrer"
variant='transparent' variant='transparent'
> >
<IconFile size={25} color={colors['blue-button']}/> <IconFile size={25} color={colors['blue-button']} />
</ActionIcon> </ActionIcon>
) : ( ) : (
<Text>Tidak ada dokumen tersedia</Text> <Text>Tidak ada dokumen tersedia</Text>
@@ -106,6 +109,14 @@ function ListAPBDes({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList'; import JudulList from '../../../_com/judulList';
@@ -50,19 +50,11 @@ function ListKategoriKegiatan({ search }: { search: string }) {
} }
} }
useEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [page, search])
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList'; import JudulList from '../../../_com/judulList';
@@ -38,22 +38,11 @@ function ListDesaAntiKorupsi({ search }: { search: string }) {
load, load,
} = listState.findMany; } = listState.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => b.createdAt - a.createdAt);
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -42,18 +42,21 @@ function ListKategoriPrestasi({ search }: { search: string }) {
} }
} }
const {
data,
page,
totalPages,
loading,
load,
} = stateKategori.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateKategori.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (stateKategori.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
);
});
if (!stateKategori.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -100,6 +103,14 @@ function ListKategoriPrestasi({ search }: { search: string }) {
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -30,20 +30,22 @@ function ListPrestasiDesa() {
function ListPrestasi({ search }: { search: string }) { function ListPrestasi({ search }: { search: string }) {
const listState = useProxy(prestasiState.prestasiDesa) const listState = useProxy(prestasiState.prestasiDesa)
const router = useRouter(); const router = useRouter();
const{
data,
page,
totalPages,
loading,
load,
} = listState.findMany
useEffect(() => { useEffect(() => {
listState.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (listState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.kategori?.name?.toLowerCase().includes(keyword)
);
});
if (!listState.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -95,6 +97,14 @@ function ListPrestasi({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList'; import JudulList from '../../../_com/judulList';
@@ -38,20 +38,11 @@ function ListMediaSosial({ search }: { search: string }) {
load, load,
} = stateMediaSosial.findMany; } = stateMediaSosial.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, search)
}, [page]) }, [page, search])
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.iconUrl?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -55,7 +55,10 @@ function DetailProgramInovasi() {
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"}>{stateProgramInovasi.findUnique.data?.description}</Text> <Text
fz={"lg"}
>{stateProgramInovasi.findUnique.data?.description}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link</Text> <Text fz={"lg"} fw={"bold"}>Link</Text>
@@ -63,6 +66,12 @@ function DetailProgramInovasi() {
href={stateProgramInovasi.findUnique.data?.link || "#"} href={stateProgramInovasi.findUnique.data?.link || "#"}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{
wordWrap: 'break-word',
whiteSpace: 'pre-wrap',
overflowWrap: 'break-word',
width: '100%'
}}
> >
{stateProgramInovasi.findUnique.data?.link || "Tidak ada link"} {stateProgramInovasi.findUnique.data?.link || "Tidak ada link"}
</a> </a>

View File

@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList'; import JudulList from '../../../_com/judulList';
@@ -38,21 +38,11 @@ function ListProgramInovasi({ search }: { search: string }) {
load, load,
} = stateProgramInovasi.findMany; } = stateProgramInovasi.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.description?.toLowerCase().includes(keyword) ||
item.link?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
if (loading || !data) { if (loading || !data) {
return ( return (

View File

@@ -1,14 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
import sdgsDesa from '../../_state/landing-page/sdgs-desa';
function SdgsDesa() { function SdgsDesa() {
@@ -39,20 +39,11 @@ function ListSdgsDesa({ search }: { search: string }) {
load, load,
} = listState.findMany; } = listState.findMany;
useEffect(() => { useShallowEffect(() => {
load(page, 10) load(page, 10, search)
}, []) }, [page, search])
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.name?.toLowerCase().includes(keyword) ||
item.jumlah?.toLowerCase().includes(keyword)
);
})
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -4,7 +4,7 @@ import colors from '@/con/colors';
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core'; import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react'; import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList'; import JudulList from '../../../_com/judulList';
@@ -39,22 +39,10 @@ function ListPegawaiPPID({ search }: { search: string }) {
} = stateOrganisasi.findMany; } = stateOrganisasi.findMany;
useEffect(() => { useEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [page, search]);
const filteredData = useMemo(() => { const filteredData = data || []
if (!data) return [];
return data.filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap?.toLowerCase().includes(keyword) ||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
item.telepon?.toLowerCase().includes(keyword) ||
item.posisi?.nama?.toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
}, [data, search]);
// Handle loading state // Handle loading state
if (loading || !data) { if (loading || !data) {

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -33,9 +33,17 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
useEffect(() => { useEffect(() => {
stateOrganisasi.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const handleHapus = async () => { const handleHapus = async () => {
if (selectedId) { if (selectedId) {
@@ -45,17 +53,9 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
} }
} }
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.nama?.toLowerCase().includes(keyword) ||
item.deskripsi?.toLowerCase().includes(keyword) ||
item.hierarki?.toString().toLowerCase().includes(keyword)
);
})
.sort((a, b) => a.hierarki - b.hierarki);
if (!stateOrganisasi.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -120,6 +120,14 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
</Table> </Table>
</Box> </Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
{/* Modal Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -1,18 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function programKemiskinanFindMany() { export default async function programKemiskinanFindMany(context: Context) {
const data = await prisma.programKemiskinan.findMany({ const page = Number(context.query.page) || 1;
include: { const limit = Number(context.query.limit) || 10;
statistik: true, // ikut sertakan relasinya const search = (context.query.search as string) || '';
}, const skip = (page - 1) * limit;
orderBy: {
createdAt: "desc", // Buat where clause
}, const where: any = { isActive: true };
});
// Tambahkan pencarian (jika ada)
return { if (search) {
success: true, where.OR = [
message: "Success get all program layanan", { nama: { contains: search, mode: 'insensitive' } },
data, { deskripsi: { contains: search, mode: 'insensitive' } },
}; ];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.programKemiskinan.findMany({
where,
include: {
statistik: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.programKemiskinan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil program kemiskinan dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data program kemiskinan",
};
}
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,23 @@ import { Context } from "elysia";
async function apbdesFindMany(context: Context) { async function apbdesFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.aPBDes.findMany({ prisma.aPBDes.findMany({
where: { isActive: true }, where,
include: { include: {
image: true, image: true,
file: true, file: true,
@@ -20,7 +32,7 @@ async function apbdesFindMany(context: Context) {
orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.aPBDes.count({ prisma.aPBDes.count({
where: { isActive: true }, where,
}), }),
]); ]);

View File

@@ -11,7 +11,7 @@ const APBDes = new Elysia({
}) })
// ✅ Find all // ✅ Find all
.get("/find-many", apbdesFindMany) .get("/findMany", apbdesFindMany)
// ✅ Find by ID // ✅ Find by ID
.get("/:id", apbdesFindUnique) .get("/:id", apbdesFindUnique)

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,23 +6,35 @@ import { Context } from "elysia";
async function desaAntiKorupsiFindMany(context: Context) { async function desaAntiKorupsiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ deskripsi: { contains: search, mode: "insensitive" } },
{ kategori: { name: { contains: search, mode: "insensitive" } } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.desaAntiKorupsi.findMany({ prisma.desaAntiKorupsi.findMany({
where: { isActive: true }, where,
include: { include: {
kategori: true, kategori: true,
file: true, file: true,
}, },
skip, skip,
take: limit, take: limit,
orderBy: { name: 'asc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { name: "asc" }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.desaAntiKorupsi.count({ prisma.desaAntiKorupsi.count({
where: { isActive: true } where,
}) }),
]); ]);
return { return {

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,18 +6,29 @@ import { Context } from "elysia";
async function kategoriDesaAntiKorupsiFindMany(context: Context) { async function kategoriDesaAntiKorupsiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.kategoriDesaAntiKorupsi.findMany({ prisma.kategoriDesaAntiKorupsi.findMany({
where: { isActive: true }, where,
skip, skip,
take: limit, take: limit,
orderBy: { name: 'asc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { name: 'asc' }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.kategoriDesaAntiKorupsi.count({ prisma.kategoriDesaAntiKorupsi.count({
where: { isActive: true } where,
}) })
]); ]);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,25 @@ import { Context } from "elysia";
async function prestasiDesaFindMany(context: Context) { async function prestasiDesaFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
{ kategori: { name: { contains: search, mode: 'insensitive' } } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.prestasiDesa.findMany({ prisma.prestasiDesa.findMany({
where: { isActive: true }, where,
include: { include: {
image: true, image: true,
kategori: true, kategori: true,
@@ -20,7 +34,7 @@ async function prestasiDesaFindMany(context: Context) {
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.prestasiDesa.count({ prisma.prestasiDesa.count({
where: { isActive: true }, where,
}), }),
]); ]);

View File

@@ -1,15 +1,52 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function kategoriPrestasiFindMany() { async function kategoriPrestasiFindMany(context: Context) {
const data = await prisma.kategoriPrestasiDesa.findMany(); const page = Number(context.query.page) || 1;
return { const limit = Number(context.query.limit) || 10;
success: true, const search = (context.query.search as string) || '';
data: data.map((item: any) => { const skip = (page - 1) * limit;
return {
id: item.id, // Buat where clause
name: item.name, const where: any = { isActive: true };
}
}), // Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
const [data, total] = await Promise.all([
prisma.kategoriPrestasiDesa.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.kategoriPrestasiDesa.count({
where,
}),
]);
return {
success: true,
message: "Success fetch Kategori Prestasi Desa with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
}; };
} } catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch Kategori Prestasi Desa with pagination",
};
}
}
export default kategoriPrestasiFindMany;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,23 @@ import { Context } from "elysia";
async function mediaSosialFindMany(context: Context) { async function mediaSosialFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.mediaSosial.findMany({ prisma.mediaSosial.findMany({
where: { isActive: true }, where,
include: { include: {
image: true, image: true,
}, },

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// // /api/berita/findManyPaginated.ts // // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,23 @@ import { Context } from "elysia";
async function programInovasiFindMany(context: Context) { async function programInovasiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.programInovasi.findMany({ prisma.programInovasi.findMany({
where: { isActive: true }, where,
include: { include: {
image: true, image: true,
}, },

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,23 @@ import { Context } from "elysia";
async function sdgsDesaFindMany(context: Context) { async function sdgsDesaFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.sDGSDesa.findMany({ prisma.sDGSDesa.findMany({
where: { isActive: true }, where,
include: { include: {
image: true, image: true,
}, },
@@ -19,7 +31,7 @@ async function sdgsDesaFindMany(context: Context) {
orderBy: { jumlah: "desc" }, // opsional, kalau mau urut berdasarkan waktu orderBy: { jumlah: "desc" }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.sDGSDesa.count({ prisma.sDGSDesa.count({
where: { isActive: true }, where,
}), }),
]); ]);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,22 +6,34 @@ import { Context } from "elysia";
export default async function pegawaiFindMany(context: Context) { export default async function pegawaiFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.pegawaiPPID.findMany({ prisma.pegawaiPPID.findMany({
where: { isActive: true }, where,
include: { include: {
posisi: true, posisi: true,
image: true, image: true,
}, },
skip, skip,
take: limit, take: limit,
orderBy: { posisi: { hierarki: 'asc' } }, orderBy: { posisi: { hierarki: 'asc' } },
}), }),
prisma.pegawaiPPID.count({ prisma.pegawaiPPID.count({
where: { isActive: true } where,
}) })
]); ]);

View File

@@ -1,17 +1,58 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
// /api/posisi-organisasi/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function posisiOrganisasiFindMany() { async function posisiOrganisasiFindMany(context: Context) {
const data = await prisma.posisiOrganisasiPPID.findMany(); const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || "";
const where: any = { isActive: true };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ deskripsi: { contains: search, mode: "insensitive" } },
];
}
try {
const whereClause = {
...where,
isActive: true
};
const [data, total] = await Promise.all([
prisma.posisiOrganisasiPPID.findMany({
where: whereClause,
skip: (page - 1) * limit,
take: limit,
orderBy: { hierarki: "asc" },
}),
prisma.posisiOrganisasiPPID.count({ where: whereClause }),
]);
return { return {
success: true, success: true,
message: "Berhasil mengambil semua data posisi organisasi", message: "Berhasil mengambil data posisi organisasi dengan pagination",
data: data.map((item: any) => ({ data: data.map((item: any) => ({
id: item.id, id: item.id,
nama: item.nama, nama: item.nama,
deskripsi: item.deskripsi, deskripsi: item.deskripsi,
hierarki: item.hierarki, hierarki: item.hierarki,
})), })),
page,
totalPages: Math.ceil(total / limit),
total,
}; };
} } catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Gagal mengambil data posisi organisasi",
};
}
}
export default posisiOrganisasiFindMany;

View File

@@ -1,21 +1,157 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, Image, Paper } from '@mantine/core'; import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import React from 'react'; import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { useShallowEffect } from '@mantine/hooks';
function Page() { function Page() {
const state = useProxy(PendapatanAsliDesa.ApbDesa);
useShallowEffect(() => {
state.findMany.load();
}, []);
useShallowEffect(() => {
PendapatanAsliDesa.pembiayaan.findMany.load();
PendapatanAsliDesa.belanja.findMany.load();
PendapatanAsliDesa.pendapatan.findMany.load();
}, []);
// Get the latest APB data
const latestApb = state.findMany.data?.[0];
// Calculate totals
const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
Pendapatan Asli Desa Pendapatan Asli Desa
</Text> </Text>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p={"xl"}> <Paper bg={colors['white-1']} p="xl">
<Image src="/pa-desa.png" alt=''/> <SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="orange">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalBelanja)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="green">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPembiayaan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
</SimpleGrid>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,192 +1,32 @@
'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Paper, Text, ColorSwatch, Flex } from '@mantine/core'; import { Stack, Box, Paper, Text, ColorSwatch, Flex, Skeleton } from '@mantine/core';
import React from 'react'; import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import { useProxy } from 'valtio/utils';
import grafikDemografiPekerjaan from '@/app/admin/(dashboard)/_state/ekonomi/demografi-pekerjaan';
import { useShallowEffect } from '@mantine/hooks';
const data = [
{
id: 1,
Pekerjaan: 'Guru',
laki: 2,
perempuan: 3
},
{
id: 2,
Pekerjaan: 'Belajar/Mahasiswa',
laki: 37,
perempuan: 38
},
{
id: 3,
Pekerjaan: 'Karyawan Bumdn',
laki: 1,
perempuan: 0
},
{
id: 4,
Pekerjaan: 'Buruh Tani/Perkebunan',
laki: 1,
perempuan: 0
},
{
id: 5,
Pekerjaan: 'Karyawan Swasta',
laki: 3,
perempuan: 17
},
{
id: 6,
Pekerjaan: 'Karyawan Honorer',
laki: 2,
perempuan: 1
},
{
id: 7,
Pekerjaan: 'Buruh Harian Lepas',
laki: 8,
perempuan: 5
},
{
id: 8,
Pekerjaan: 'Belum/Tidak Bekerja',
laki: 87,
perempuan: 44
},
{
id: 9,
Pekerjaan: ' Kepolisian RI (Polri)',
laki: 4,
perempuan: 0
},
{
id: 10,
Pekerjaan: 'Wiraswasta Mengurus Rumah Tangga',
laki: 1,
perempuan: 7
},
{
id: 11,
Pekerjaan: 'Dosen',
laki: 1,
perempuan: 1
},
{
id: 12,
Pekerjaan: 'Perangkat Desa',
laki: 17,
perempuan: 19
},
{
id: 13,
Pekerjaan: 'Nelayan',
laki: 3,
perempuan: 0
},
{
id: 14,
Pekerjaan: 'Penyuluh Pertanian',
laki: 33,
perempuan: 24
},
{
id: 15,
Pekerjaan: 'Tukang Las/Pandai Besi',
laki: 5,
perempuan: 0
},
{
id: 16,
Pekerjaan: 'Sopir/Driver',
laki: 10,
perempuan: 3
},
{
id: 17,
Pekerjaan: 'Teknisi/Listrik',
laki: 25,
perempuan: 0
},
{
id: 18,
Pekerjaan: 'Montir/Mekanik',
laki: 25,
perempuan: 0
},
{
id: 19,
Pekerjaan: 'Karyawan Hotel/Pariwisata',
laki: 2,
perempuan: 52
},
{
id: 20,
Pekerjaan: 'Pengrajin (Batik, Anyaman, Kayu)',
laki: 5,
perempuan: 25
},
{
id: 21,
Pekerjaan: 'Tukang Bangunan',
laki: 25,
perempuan: 5
},
{
id: 22,
Pekerjaan: 'Tukang Kayu/Furnitur',
laki: 25,
perempuan: 0
},
{
id: 23,
Pekerjaan: 'Penjahit',
laki: 2,
perempuan: 35
},
{
id: 24,
Pekerjaan: 'Pedagang Pasar',
laki: 25,
perempuan: 30
},
{
id: 25,
Pekerjaan: 'Warung Makan/Penjual Makanan',
laki: 15,
perempuan: 30
},
{
id: 26,
Pekerjaan: 'Satpam/Security',
laki: 20,
perempuan: 5
},
{
id: 27,
Pekerjaan: 'Pengusaha Kecil (UMKM)',
laki: 5,
perempuan: 20
},
{
id: 28,
Pekerjaan: 'Karyawan Restoran/Kafe',
laki: 5,
perempuan: 15
},
{
id: 29,
Pekerjaan: 'Freelancer',
laki: 20,
perempuan: 10
},
{
id: 30,
Pekerjaan: 'Fotografer',
laki: 25,
perempuan: 9
},
]
function Page() { function Page() {
const state = useProxy(grafikDemografiPekerjaan)
const {
data,
} = state.findMany
useShallowEffect(() => {
state.findMany.load()
}, [])
if (!data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -207,7 +47,13 @@ function Page() {
p={10} p={10}
mb={50} mb={50}
h={400} h={400}
data={data} w={150}
data={data.map((item) => ({
id: item.id,
Pekerjaan: item.pekerjaan,
laki: item.lakiLaki,
perempuan: item.perempuan,
}))}
dataKey="Pekerjaan" dataKey="Pekerjaan"
series={[ series={[
{ name: 'laki', color: '#5082EE' }, { name: 'laki', color: '#5082EE' },

View File

@@ -1,119 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, CheckIcon, Combobox, ComboboxChevron, ComboboxOption, ComboboxOptions, ComboboxTarget, Group, InputBase, InputPlaceholder, Paper, SimpleGrid, Stack, Text, useCombobox } from '@mantine/core';
import { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts';
const data = [
{
id: 1,
tahun: '2024',
Penduduk: 400000
},
{
id: 2,
tahun: '2025',
Penduduk: 450000
},
]
const tahun = [
'2024',
'2025'
];
function Page() {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: (eventSource) => {
if (eventSource === 'keyboard') {
combobox.selectActiveOption();
} else {
combobox.updateSelectedOptionIndex('active');
}
},
});
const [value, setValue] = useState<string | null>('2024');
const options = tahun.map((item) => (
<ComboboxOption value={item} key={item} active={item === value}>
<Group gap="xs">
{item === value && <CheckIcon size={12} />}
<span>{item}</span>
</Group>
</ComboboxOption>
));
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Penduduk Miskin Tahun 2024-2025
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<SimpleGrid
pb={20}
cols={{
base: 1,
md: 2
}}
>
<Paper p={'xl'}>
<Text fz={'h3'}>Tahun: 2024</Text>
<Text fw={"bold"} fz={'h1'}>4,800,000 Orang</Text>
</Paper>
<Paper p={'xl'}>
<Text>Pilih Tahun</Text>
<Combobox
store={combobox}
resetSelectionOnOptionHover
withinPortal={false}
onOptionSubmit={(val) => {
setValue(val);
combobox.updateSelectedOptionIndex('active');
}}
>
<ComboboxTarget targetType="button">
<InputBase
component="button"
type="button"
pointer
rightSection={<ComboboxChevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
>
{value || <InputPlaceholder>Pick value</InputPlaceholder>}
</InputBase>
</ComboboxTarget>
<Combobox.Dropdown>
<ComboboxOptions>{options}</ComboboxOptions>
</Combobox.Dropdown>
</Combobox>
</Paper>
</SimpleGrid>
<Paper p={'xl'}>
<Text pb={10} fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text>
<BarChart
p={10}
h={300}
data={data}
dataKey="tahun"
series={[
{ name: 'Penduduk', color: '#8785D3' },
]}
tickLine="y"
/>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,82 @@
'use client'
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() {
type JPMGrafik = {
id: string;
year: number;
totalPoorPopulation: number;
}
const state = useProxy(jumlahPendudukMiskin)
const [chartData, setChartData] = useState<JPMGrafik[]>([])
useShallowEffect(() => {
state.findMany.load()
}, [])
useEffect(() => {
if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({
id: item.id,
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation),
})));
}
}, [state.findMany.data]);
if (!state.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Penduduk Miskin
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<Paper p={'xl'}>
<Text fz={'h3'}>Jumlah Data Penduduk Miskin</Text>
<Text fw={"bold"} fz={'h1'}>
{state.findMany.data?.reduce((sum, item) => sum + (Number(item.totalPoorPopulation) || 0), 0).toLocaleString()} Orang
</Text>
</Paper>
<Paper p={'xl'}>
<Text pb={10} fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text>
<BarChart
h={300}
data={chartData}
dataKey="year"
series={[
{ name: 'totalPoorPopulation', color: 'blue.6', label: 'Jumlah Penduduk Miskin' },
]}
xAxisLabel="Tahun"
yAxisLabel="Jumlah Penduduk"
/>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,62 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, Center, Paper, ColorSwatch, Flex } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { PieChart } from '@mantine/charts'; import { PieChart } from '@mantine/charts';
import { Box, Center, ColorSwatch, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
const datausiaKerja = [
{
id: 1,
name: '18 - 25',
value: 45,
color: 'indigo.6'
},
{
id: 2,
name: '26 - 35',
value: 35,
color: 'teal.6'
},
{
id: 3,
name: '36 - 45',
value: 15,
color: 'yellow.6'
},
{
id: 4,
name: '46+',
value: 5,
color: 'red.6'
},
]
const datakerjaPendidikan = [
{
id: 1,
name: 'SD',
value: 10,
color: 'indigo.6'
},
{
id: 2,
name: 'SMP',
value: 20,
color: 'teal.6'
},
{
id: 3,
name: 'SMA/SMK',
value: 45,
color: 'yellow.6'
},
{
id: 4,
name: 'D3/S1',
value: 25,
color: 'red.6'
},
]
function Page() { function Page() {
const stateGrafikNganggur = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur)
const stateGrafikNganggurPendidikan = useProxy(grafikNganggur.grafikBerdasarkanPendidikan)
const [donutGrafikNganggurData, setDonutGrafikNganggurData] = useState<any[]>([])
const [donutGrafikNganggurDataPendidikan, setDonutGrafikNganggurDataPendidikan] = useState<any[]>([])
const [mounted, setMounted] = useState(false)
const [mounted2, setMounted2] = useState(false)
useShallowEffect(() => {
setMounted(true)
setMounted2(true)
stateGrafikNganggur.findMany.load()
stateGrafikNganggurPendidikan.findMany.load()
}, [])
useEffect(() => {
if (stateGrafikNganggur.findMany.data) {
const totalUsia18_25 = stateGrafikNganggur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia18_25 || 0), 0);
const totalUsia26_35 = stateGrafikNganggur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
const totalUsia36_45 = stateGrafikNganggur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
const totalUsia46_keatas = stateGrafikNganggur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
setDonutGrafikNganggurData([
{ name: 'usia 18 - 25', value: totalUsia18_25, color: '#4b6Ef5', key: 'usia18_25' },
{ name: 'usia 26 - 35', value: totalUsia26_35, color: '#14b885', key: 'usia26_35' },
{ name: 'usia 36 - 45', value: totalUsia36_45, color: '#E6A03B', key: 'usia36_45' },
{ name: 'usia 46 +', value: totalUsia46_keatas, color: '#DB524D', key: 'usia46_keatas' },
])
}
}, [stateGrafikNganggur.findMany.data])
useEffect(() => {
if (stateGrafikNganggurPendidikan.findMany.data) {
const SD = stateGrafikNganggurPendidikan.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0);
const SMP = stateGrafikNganggurPendidikan.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0);
const SMA = stateGrafikNganggurPendidikan.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0);
const D3 = stateGrafikNganggurPendidikan.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stateGrafikNganggurPendidikan.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
setDonutGrafikNganggurDataPendidikan([
{ name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
{ name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
{ name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]);
}
}, [stateGrafikNganggurPendidikan.findMany.data])
if (!stateGrafikNganggur.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
if (!stateGrafikNganggur.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -71,9 +85,18 @@ function Page() {
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
<Paper p={'lg'}> <Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text> <Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
<Center> {mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart size={300} withLabelsLine labelsPosition="outside" labelsType="percent" withLabels data={datausiaKerja} withTooltip tooltipDataSource="segment" mx="auto" /> <PieChart
</Center> size={300}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurData}
withTooltip
tooltipDataSource="segment"
mx="auto" />
</Box>) : <Skeleton h={500} />}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}> <Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box> <Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}> <Flex gap={{ base: 5, md: 8 }} align={'center'}>
@@ -103,9 +126,18 @@ function Page() {
</Paper> </Paper>
<Paper p={'lg'}> <Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text> <Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text>
<Center> {mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (<Center>
<PieChart size={300} withLabelsLine labelsPosition="outside" labelsType="percent" withLabels data={datakerjaPendidikan} withTooltip tooltipDataSource="segment" mx="auto" /> <PieChart
</Center> size={300}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurDataPendidikan}
withTooltip
tooltipDataSource="segment"
mx="auto" />
</Center>) : <Skeleton h={500} />}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}> <Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box> <Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}> <Flex gap={{ base: 5, md: 8 }} align={'center'}>
@@ -127,10 +159,16 @@ function Page() {
</Box> </Box>
<Box> <Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}> <Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3/S1</Text> <Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3</Text>
<ColorSwatch color="#DB524D" size={30} /> <ColorSwatch color="#DB524D" size={30} />
</Flex> </Flex>
</Box> </Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>S1</Text>
<ColorSwatch color="#1018A8FF" size={30} />
</Flex>
</Box>
</Flex> </Flex>
</Paper> </Paper>
</Stack> </Stack>

View File

@@ -1,177 +0,0 @@
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Button, Center, ColorSwatch, Flex, Group, Paper, SimpleGrid, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconChevronDown, IconDownload, IconSchool, IconSchoolOff, IconUserOff } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto';
const data1 = [
{
id: 1,
icon: <IconUserOff size={35} />,
judul: 'Total Pengangguran',
jumlah: '140',
persentase: <Text fz={'h4'} c={'green'}>-12.5% dari 2024</Text>
},
{
id: 2,
icon: <IconSchool size={35} />,
judul: 'Pengangguran Terdidik',
jumlah: '80',
persentase: <Text fz={'h4'} c={'gray'}>57.1% dari total</Text>
},
{
id: 3,
icon: <IconSchoolOff size={35} />,
judul: 'Pengangguran Tidak Terdidik',
jumlah: '60',
persentase: <Text fz={'h4'} c={'gray'}>42.9% dari total</Text>
},
]
const dataPengangguran = [
{
id: 1,
bulan: 'Jan',
berpendidikan: 98,
takberpendidikan: 74,
},
{
id: 2,
bulan: 'Feb',
berpendidikan: 85,
takberpendidikan: 74,
},
{
id: 3,
bulan: 'Mar',
berpendidikan: 76,
takberpendidikan: 55,
},
{
id: 4,
bulan: 'Apr',
berpendidikan: 98,
takberpendidikan: 74,
},
{
id: 5,
bulan: 'Mei',
berpendidikan: 74,
takberpendidikan: 54,
},
{
id: 6,
bulan: 'Jun',
berpendidikan: 55,
takberpendidikan: 50,
},
]
const dataTable = [
{ bulan: 'Jan', total: 160, terdidik: 95, takterdidik: 65, perubahan: '-' },
{ bulan: 'Feb', total: 155, terdidik: 90, takterdidik: 65, perubahan: '-3.1%' },
{ bulan: 'Mar', total: 150, terdidik: 88, takterdidik: 62, perubahan: '-3.2%' },
{ bulan: 'Apr', total: 148, terdidik: 85, takterdidik: 63, perubahan: '-1.3%' },
{ bulan: 'Mei', total: 145, terdidik: 82, takterdidik: 63, perubahan: '-2.0%' },
{ bulan: 'Jun', total: 140, terdidik: 80, takterdidik: 60, perubahan: '-3.4%' },
]
function Page() {
const rows = dataTable.map((element) => (
<TableTr key={element.bulan}>
<TableTd ta={'center'}>{element.bulan}</TableTd>
<TableTd ta={'center'}>{element.total}</TableTd>
<TableTd ta={'center'}>{element.terdidik}</TableTd>
<TableTd ta={'center'}>{element.takterdidik}</TableTd>
<TableTd ta={'center'}>{element.perubahan}</TableTd>
</TableTr>
));
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Pengangguran 2024 - 2025
</Text>
<Group py={20} align='center' justify='space-between'>
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
<Flex gap={'xl'}>
<Button c={'black'} bg={colors['white-1']} rightSection={<IconChevronDown size={20} />}>2025</Button>
<Button leftSection={<IconDownload size={20} />}>Export</Button>
</Flex>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<SimpleGrid
cols={1}
pb={20}
>
{data1.map((v, k) => {
return (
<Paper px={25} key={k} py={'lg'} bg={colors['white-1']} shadow={'md'}>
<Flex justify={'space-between'} align={'center'}>
<Box>
<Stack>
<Text fz={'h4'}>{v.judul}</Text>
<Text fz={'h2'} fw={'bold'}>{v.jumlah}</Text>
{v.persentase}
</Stack>
</Box>
{v.icon}
</Flex>
</Paper>
)
})}
</SimpleGrid>
<Paper p={'lg'}>
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Berpendidikan</Text>
<ColorSwatch color="#5082EE" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Tak Berpendidikan</Text>
<ColorSwatch color="#DA524C" size={30} />
</Flex>
</Box>
</Flex>
<Center>
<BarChart
p={10}
h={400}
data={dataPengangguran}
dataKey="bulan"
series={[
{ name: 'berpendidikan', color: '#5082EE' },
{ name: 'takberpendidikan', color: '#DA524C' },
]}
tickLine="y"
/>
</Center>
</Paper>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h4'}>Detail Data Pengangguran</Text>
<Table striped highlightOnHover>
<TableThead>
<TableTr>
<TableTh ta={'center'}>Bulan</TableTh>
<TableTh ta={'center'}>Total</TableTh>
<TableTh ta={'center'}>Terdidik</TableTh>
<TableTh ta={'center'}>Tidak Terdidik</TableTh>
<TableTh ta={'center'}>Perubahan</TableTh>
</TableTr>
</TableThead>
<TableTbody>{rows}</TableTbody>
</Table>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,182 @@
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, ColorSwatch, Flex, Group, Paper, SimpleGrid, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconSchool, IconSchoolOff, IconUserOff } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
type DataPengangguran = {
id: string;
bulan: string;
berpendidikan: number;
takberpendidikan: number;
}
function Page() {
const [chartData, setChartData] = useState<DataPengangguran[]>([])
const [mounted, setMounted] = useState(false)
const state = useProxy(jumlahPengangguranState.jumlahPengangguran)
// const [selectedYear, setSelectedYear] = useState<string | null>(null);
useShallowEffect(() => {
setMounted(true)
state.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({
id: item.id,
bulan: item.month,
berpendidikan: Number(item.educatedUnemployment),
takberpendidikan: Number(item.uneducatedUnemployment),
})));
}
}, [state.findMany.data]);
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Jumlah Pengangguran
</Text>
<Group py={20} align='center' justify='space-between'>
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'>
<SimpleGrid
cols={1}
pb={20}
>
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Total Unemployment Card */}
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconUserOff size={35} color={colors['blue-button']} />
<Text fz="h4" fw={600}>Total Pengangguran</Text>
<Text fz="h2" fw={700} c={colors['blue-button']}>
{state.findMany.data?.[0]?.totalUnemployment || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
Data tahun {state.findMany.data?.[0]?.year || ''}
</Text>
</Flex>
</Paper>
{/* Educated Unemployment Card */}
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconSchool size={35} color="#5082EE" />
<Text fz="h4" fw={600}>Pengangguran Terdidik</Text>
<Text fz="h2" fw={700} c="#5082EE">
{state.findMany.data?.[0]?.educatedUnemployment || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
{state.findMany.data?.[0]?.totalUnemployment ?
`${Math.round((state.findMany.data[0].educatedUnemployment / state.findMany.data[0].totalUnemployment) * 100)}% dari total` :
'0% dari total'}
</Text>
</Flex>
</Paper>
{/* Uneducated Unemployment Card */}
<Paper px={25} py={'lg'} bg={colors['white-1']} shadow="md">
<Flex direction="column" gap="md">
<IconSchoolOff size={35} color="#DA524C" />
<Text fz="h4" fw={600}>Pengangguran Tidak Terdidik</Text>
<Text fz="h2" fw={700} c="#DA524C">
{state.findMany.data?.[0]?.uneducatedUnemployment || 0} Orang
</Text>
<Text fz="sm" c="dimmed">
{state.findMany.data?.[0]?.totalUnemployment ?
`${Math.round((state.findMany.data[0].uneducatedUnemployment / state.findMany.data[0].totalUnemployment) * 100)}% dari total` :
'0% dari total'}
</Text>
</Flex>
</Paper>
</SimpleGrid>
</SimpleGrid>
<Paper p={'lg'}>
<Flex pb={30} justify={'flex-end'} gap={'xl'} align={'center'}>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Berpendidikan</Text>
<ColorSwatch color="#5082EE" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}>
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>Pengangguran Tak Berpendidikan</Text>
<ColorSwatch color="#DA524C" size={30} />
</Flex>
</Box>
</Flex>
{!mounted || chartData.length === 0 ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="bulan"
series={[
{ name: 'berpendidikan', color: '#5082EE', label: 'Terdidik' },
{ name: 'takberpendidikan', color: '#DA524C', label: 'Tidak Terdidik' },
]}
/>
</Box>
</Paper>
</Box>
)}
</Paper>
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h4'}>Detail Data Pengangguran</Text>
<Table striped highlightOnHover>
<TableThead>
<TableTr>
<TableTh ta={'center'}>Bulan</TableTh>
<TableTh ta={'center'}>Total</TableTh>
<TableTh ta={'center'}>Terdidik</TableTh>
<TableTh ta={'center'}>Tidak Terdidik</TableTh>
<TableTh ta={'center'}>Perubahan</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{state.findMany.data?.map((item, index) => (
<TableTr key={item?.id ? String(item.id) : `row-${index}`}>
<TableTd ta={'center'}>{item.month}</TableTd>
<TableTd ta={'center'}>{item.totalUnemployment}</TableTd>
<TableTd ta={'center'}>{item.educatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.uneducatedUnemployment}</TableTd>
<TableTd ta={'center'}>{item.percentageChange}</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -1,54 +1,50 @@
'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper } from '@mantine/core'; import { Stack, Box, Text, SimpleGrid, Paper, Skeleton, Center } from '@mantine/core';
import React from 'react'; import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { LineChart } from '@mantine/charts'; import { CartesianGrid, Legend, Line, LineChart as RechartsLineChart, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks';
interface StatistikData {
id: string;
tahun: number;
jumlah: number;
createdAt: Date;
updatedAt: Date;
}
interface ProgramKemiskinanData {
id: string;
nama: string;
deskripsi: string;
ikonUrl: string | null;
statistik: StatistikData | null;
isActive: boolean;
statistikId: string | null;
createdAt: Date;
updatedAt: Date;
}
const data = [
{
id: 1,
judul: 'Bantuan Tunai',
deskripsi: 'Bantuan keuangan langsung bagi keluarga kurang mampu'
},
{
id: 2,
judul: 'Pelatihan Kerja',
deskripsi: 'Program pelatihan keterampilan untuk meningkatkan peluang kerja'
},
{
id: 3,
judul: 'Subsidi Pangan',
deskripsi: 'Distribusi bahan pangan bersubsidi bagi masyarakat kurang mampu'
},
{
id: 4,
judul: 'Layanan Kesehatan Gratis',
deskripsi: 'Akses kesehatan gratis bagi masyarakat kurang mampu'
},
]
const dataStatistik = [
{
id: 1,
tahun: '2022',
Kemiskinan: 400000
},
{
id: 2,
tahun: '2023',
Kemiskinan: 450000
},
{
id: 3,
tahun: '2024',
Kemiskinan: 500000
},
{
id: 4,
tahun: '2025',
Kemiskinan: 400000
},
]
function Page() { function Page() {
const state = useProxy(programKemiskinanState)
useShallowEffect(() => {
state.findMany.load()
}, [])
const data = programKemiskinanState.findMany.data
if (!data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -69,26 +65,58 @@ function Page() {
md: 2 md: 2
}} }}
> >
{data.map((v, k) => { {state.findMany.data.map((v, k) => {
return ( return (
<Paper p={'xl'} key={k}> <Paper p={'xl'} key={k}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text> <Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
<Text fz={'lg'} c={'black'}>{v.deskripsi}</Text> <Text fz={'lg'} c={'black'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
</Paper> </Paper>
) )
})} })}
</SimpleGrid> </SimpleGrid>
<Paper p={'xl'}> <Paper p={'xl'}>
<Text fz={'h4'} fw={'bold'} c={colors['blue-button']}>Statistik Kemiskinan Masyarakat</Text> <Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
<LineChart <Box style={{ width: '100%', height: 'auto' }}>
h={300} {state.findMany.data.length > 0 && state.findMany.data[0]?.statistik ? (
data={dataStatistik} <Box w="100%" style={{ overflowX: 'auto' }}>
dataKey="tahun" <Center>
series={[ <RechartsLineChart
{ name: 'Kemiskinan', color: colors['blue-button'] }, width={800}
]} height={300}
curveType="linear" data={state.findMany.data
/> .filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } =>
item.statistik !== null
)
.map(item => ({
tahun: item.statistik.tahun,
jumlah: item.statistik.jumlah
}))
.sort((a, b) => a.tahun - b.tahun)
}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip
formatter={(value: number, name: string) => [`${value} orang`, name]}
labelFormatter={(label: number) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah Masyarakat Miskin"
stroke={colors['blue-button']}
activeDot={{ r: 8 }}
/>
</RechartsLineChart>
</Center>
</Box>
) : (
<Text c="dimmed">Belum ada data statistik yang tersedia</Text>
)}
</Box>
</Paper> </Paper>
</Stack> </Stack>
</Box> </Box>

View File

@@ -1,23 +1,33 @@
'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Text, Paper } from '@mantine/core'; import { Stack, Box, Text, Paper, Skeleton } from '@mantine/core';
import React from 'react'; import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
import { useShallowEffect } from '@mantine/hooks';
const data = [
{
id: 1,
sektor: 'Sektor Pertanian',
Ton: 20
},
{
id: 2,
sektor: 'Sektor Peternakan',
Ton: 5
},
]
function Page() { function Page() {
const state = useProxy(grafikSektorUnggulan)
const {
data,
loading,
} = state.findMany
useShallowEffect(() => {
state.findMany.load()
}, [])
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -31,28 +41,24 @@ function Page() {
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
<Paper p={'xl'}> {data.map((v, k) => {
<Text fw={'bold'} fz={'h4'}>Jumlah Penduduk Miskin Per Tahun</Text> return (
<Text fz={'h4'} ta={'justify'}> <Paper p={'xl'} key={k}>
Pertanian di Darmasaba berfokus pada padi, sayuran, dan hortikultura yang dikembangkan dengan metode pertanian <Text fw={'bold'} fz={'h4'}>{v.name}</Text>
organik serta sistem irigasi tradisional yang efisien. Keberlanjutan dalam pertanian juga didukung dengan pemanfaatan <Text fz={'h4'} ta={'justify'} dangerouslySetInnerHTML={{ __html: v.description || '' }} />
teknologi modern untuk meningkatkan produktivitas hasil panen. </Paper>
</Text> )
</Paper> })}
<Paper p={'xl'}>
<Text fw={'bold'} fz={'h4'}>Sektor Peternakan</Text>
<Text fz={'h4'} ta={'justify'}>
Di bidang peternakan, Desa Darmasaba memiliki potensi besar dalam pengembangan sapi, ayam, dan babi. Sistem
peternakan yang diterapkan mengutamakan pengelolaan pakan alami dan perawatan hewan yang sehat, sehingga
menghasilkan produk ternak berkualitas tinggi.
</Text>
</Paper>
<Paper p={'xl'}> <Paper p={'xl'}>
<Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text> <Text pb={10} fw={'bold'} fz={'h4'}>Statistik Sektor Unggulan Darmasaba</Text>
<BarChart <BarChart
p={10} p={10}
h={300} h={300}
data={data} data={data.map((item) => ({
id: item.id,
sektor: item.name,
Ton: item.value,
}))}
dataKey="sektor" dataKey="sektor"
series={[ series={[
{ name: 'Ton', color: colors['blue-button'] }, { name: 'Ton', color: colors['blue-button'] },

View File

@@ -25,9 +25,9 @@ function ArtikelKesehatanPage() {
<Box> <Box>
<Paper p={'xl'} h={'112vh'} bg={colors['white-trans-1']}> <Paper p={'xl'} h={'112vh'} bg={colors['white-trans-1']}>
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text>
{state.findMany.data.map((item) => ( {state.findMany.data.map((item) => (
<Box key={item.id}> <Box key={item.id}>
<Text ta={'center'} fw={"bold"} fz={'h3'} c={colors['blue-button']}>Artikel Kesehatan</Text>
<Image pt={5} src={'/api/img/dbd.png'} alt="" /> <Image pt={5} src={'/api/img/dbd.png'} alt="" />
<Text fz={'h4'} fw={'bold'} > <Text fz={'h4'} fw={'bold'} >
{item.title} {item.title}

View File

@@ -2,7 +2,7 @@
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Divider, Group, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Divider, List, ListItem, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -139,14 +139,14 @@ function Page() {
Email : <Text span fz={'h4'}>puskesmasabiansemal3@gmail.com</Text> Email : <Text span fz={'h4'}>puskesmasabiansemal3@gmail.com</Text>
</Text> </Text>
</Paper> </Paper>
<Paper p={'lg'} w={{ base: "100%", md: "100%" }}> {/* <Paper p={'lg'} w={{ base: "100%", md: "100%" }}>
<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3945.272172359321!2d115.21836257533302!3d-8.569807186941553!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23e9d99b9395f%3A0xb002795fdcb33b30!2sUPTD%20Puskesmas%20Abiansemal%20III!5e0!3m2!1sid!2sid!4v1744792682341!5m2!1sid!2sid" width="600" height="450" style={{ border: 2, width: "100%", borderRadius: 10 }} loading="lazy"></iframe> <iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3945.272172359321!2d115.21836257533302!3d-8.569807186941553!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23e9d99b9395f%3A0xb002795fdcb33b30!2sUPTD%20Puskesmas%20Abiansemal%20III!5e0!3m2!1sid!2sid!4v1744792682341!5m2!1sid!2sid" width="600" height="450" style={{ border: 2, width: "100%", borderRadius: 10 }} loading="lazy"></iframe>
</Paper> </Paper>
<Group> <Group>
<Button fz={'lg'} bg={colors['blue-button']}> <Button fz={'lg'} bg={colors['blue-button']}>
Download Brosur Layanan (PDF) Download Brosur Layanan (PDF)
</Button> </Button>
</Group> </Group> */}
</Stack> </Stack>
</Box> </Box>
</Paper> </Paper>

View File

@@ -1,87 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import colors from '@/con/colors';
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
function GrafikBerdasarkanJenisKelamin() {
const stategrafikBerdasarkanJenisKelamin = useProxy(grafikBerdasarkanJenisKelamin)
const [mounted, setMounted] = useState(false);
const [donutData, setDonutData] = useState<any[]>([]);
useEffect(() => {
setMounted(true);
}, [])
const updateChartData = (data: any) => {
if (data && data.length > 0) {
const totalLaki = data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
const totalPerempuan = data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
setDonutData([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'], key: 'laki-laki' },
{ name: 'Perempuan', value: totalPerempuan, color: '#FF6384', key: 'perempuan' }
]);
}
};
useShallowEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
await stategrafikBerdasarkanJenisKelamin.findMany.load();
if (stategrafikBerdasarkanJenisKelamin.findMany.data) {
updateChartData(stategrafikBerdasarkanJenisKelamin.findMany.data);
}
};
if(!stategrafikBerdasarkanJenisKelamin.findMany.data) return <Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
<Skeleton h={500} />
</Stack>
return (
<Stack gap={"xl"}>
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
{mounted && donutData.length > 0 && (
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
<Center>
<PieChart
width={1000} height={530}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
innerRadius={120}
outerRadius={230}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</Center>
<Flex gap={"md"} align={"center"}>
<Box bg={'#FF6384'} w={20} h={20} />
<Text>Perempuan: {donutData.find((entry) => entry.name === 'Perempuan')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Laki-laki: {donutData.find((entry) => entry.name === 'Laki-laki')?.value}</Text>
</Flex>
</Box>
)}
</Stack>
);
}
export default GrafikBerdasarkanJenisKelamin;

View File

@@ -1,96 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikBerdasarkanResponden from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import colors from '@/con/colors';
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
function GrafikBerdasarkanResponden() {
const stategrafikBerdasarkanResponden = useProxy(grafikBerdasarkanResponden)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, [])
const updateChartData = (data: any) => {
if (data && data.length > 0) {
const totalSangatBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
const totalTidakBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
setDonutData([
{ name: 'sangatbaik', value: totalSangatBaik, color: colors['blue-button'], key: 'sangatbaik' },
{ name: 'baik', value: totalBaik, color: '#10A85AFF', key: 'baik' },
{ name: 'kurangbaik', value: totalKurangBaik, color: '#B3AA12FF', key: 'kurangbaik' },
{ name: 'tidakbaik', value: totalTidakBaik, color: '#B21313FF', key: 'tidakbaik' }
]);
}
};
useShallowEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
await stategrafikBerdasarkanResponden.findMany.load();
if (stategrafikBerdasarkanResponden.findMany.data) {
updateChartData(stategrafikBerdasarkanResponden.findMany.data);
}
};
if (!stategrafikBerdasarkanResponden.findMany.data) return <Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Skeleton h={500} />
</Stack>
return (
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && donutData.length > 0 && (
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
<Center>
<PieChart
width={1000} height={530}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
innerRadius={120}
outerRadius={230}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</Center>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Sangat Baik: {donutData.find((entry) => entry.name === 'sangatbaik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Baik: {donutData.find((entry) => entry.name === 'baik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B3AA12FF'} w={20} h={20} />
<Text>Kurang Baik: {donutData.find((entry) => entry.name === 'kurangbaik')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B21313FF'} w={20} h={20} />
<Text>Tidak Baik: {donutData.find((entry) => entry.name === 'tidakbaik')?.value}</Text>
</Flex>
</Box>
)}
</Stack>
);
}
export default GrafikBerdasarkanResponden;

View File

@@ -1,97 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
function GrafikBerdasarakanUmur() {
const stategrafikBerdasarkanUmur = useProxy(grafikBerdasarkanUmur)
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const updateChartData = (data: any) => {
if (data && data.length > 0) {
const totalRemaja = data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
const totalDewasa = data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
const totalOrangtua = data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
const totalLansia = data.reduce((acc: number, cur: any) => acc + Number(cur.lansia || 0), 0);
setDonutData([
{ name: 'Remaja', value: totalRemaja, color: colors['blue-button'], key: 'remaja' },
{ name: 'Dewasa', value: totalDewasa, color: '#D32711FF', key: 'dewasa' },
{ name: 'Orangtua', value: totalOrangtua, color: '#B46B04FF', key: 'orangtua' },
{ name: 'Lansia', value: totalLansia, color: '#038617FF', key: 'lansia' }
]);
}
};
useShallowEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
await stategrafikBerdasarkanUmur.findMany.load();
if (stategrafikBerdasarkanUmur.findMany.data) {
updateChartData(stategrafikBerdasarkanUmur.findMany.data);
}
}
if(!stategrafikBerdasarkanUmur.findMany.data) return <Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Umur Responden</Title>
<Skeleton h={500} />
</Stack>
return (
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Umur Responden</Title>
{mounted && donutData.length > 0 && (
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
<Center>
<PieChart
width={1000} height={530}
data={donutData}
>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
innerRadius={120}
outerRadius={230}
label={true}
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</Center>
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>17 - 25 tahun: {donutData.find((entry) => entry.name === 'remaja')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#D32711FF'} w={20} h={20} />
<Text>26 - 45 tahun: {donutData.find((entry) => entry.name === 'dewasa')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#B46B04FF'} w={20} h={20} />
<Text>46 - 60 tahun: {donutData.find((entry) => entry.name === 'orangtua')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#038617FF'} w={20} h={20} />
<Text>di atas 60 tahun: {donutData.find((entry) => entry.name === 'lansia')?.value}</Text>
</Flex>
</Box>
)}
</Stack>
);
}
export default GrafikBerdasarakanUmur;

View File

@@ -1,57 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import grafikHasilKepuasanMasyarakat from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
import colors from '@/con/colors';
import { Box, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
function GrafikHasilKepuasan() {
const grafikHasilKepuasan = useProxy(grafikHasilKepuasanMasyarakat)
const [chartData, setChartData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
useEffect(() => {
setMounted(true);
}, [])
useShallowEffect(() => {
const fetchData = async () => {
await grafikHasilKepuasan.findMany.load();
if (grafikHasilKepuasan.findMany.data && grafikHasilKepuasan.findMany.data.length > 0) {
setChartData(grafikHasilKepuasan.findMany.data);
}
};
fetchData();
}, []);
if(!grafikHasilKepuasan.findMany.data) return <Stack>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<Skeleton h={500} />
</Stack>
return (
<Stack gap={"xl"}>
<Text fw={"bold"} fz={{ base: 'h4', md: 'h3' }} ta={"center"}>
Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik
</Text>
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 300 : isTablet ? 400 : 450} height={380} data={chartData} >
<XAxis dataKey="label" />
<YAxis />
<Tooltip />
<Legend style={{justifyContent: 'center'}} />
<Bar dataKey="kepuasan" fill={colors['blue-button']} name="Kepuasan" />
</BarChart>
)}
</Box>
</Stack>
);
}
export default GrafikHasilKepuasan;

View File

@@ -1,47 +1,674 @@
import colors from '@/con/colors'; /* eslint-disable @typescript-eslint/no-explicit-any */
import { Box, Paper, Stack, Text } from '@mantine/core'; "use client";
import BackButton from '../../desa/layanan/_com/BackButto'; import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
import GrafikBerdasarkanJenisKelamin from './grafik_berdasarkan_jenis_kelamin/page'; import colors from "@/con/colors";
import GrafikBerdasarkanResponden from './grafik_berdasarkan_pilihan_responden/page'; import { BarChart, PieChart } from '@mantine/charts';
import GrafikBerdasarakanUmur from './grafik_berdasarkan_umur_responden/page'; import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
import GrafikHasilKepuasan from './grafik_hasil_kepuasan_masyarakat/page'; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { useState } from "react";
import { useProxy } from "valtio/utils";
function Page() { interface ChartDataItem {
name: string;
value: number;
color: string;
label?: string;
}
function Kepuasan() {
const state = useProxy(indeksKepuasanState.responden);
const { data, loading } = state.findMany;
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
const [opened, { open, close }] = useDisclosure(false)
const resetForm = () => {
state.create.form = {
...state.create.form,
name: "",
tanggal: "",
jenisKelaminId: "",
ratingId: "",
kelompokUmurId: "",
}
}
useShallowEffect(() => {
indeksKepuasanState.jenisKelaminResponden.findMany.load()
indeksKepuasanState.pilihanRatingResponden.findMany.load()
indeksKepuasanState.kelompokUmurResponden.findMany.load()
})
const handleSubmit = async () => {
try {
const id = await state.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await state.findUnique.load(idStr);
}
resetForm();
close()
} catch (error) {
console.error('Error submitting form:', error);
}
}
// Load data on component mount
useShallowEffect(() => {
if (!data && !loading) {
state.findMany.load(1, 1000); // Load first page with a large limit to get all data
return;
}
if (data && data.length > 0) {
// Hitung total berdasarkan jenis kelamin
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
// Hitung total berdasarkan rating
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
// Hitung total berdasarkan kelompok umur
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
// Update gender chart data
setDonutDataJenisKelamin([
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
]);
// Update rating chart data
setDonutDataRating([
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
]);
// Update age group chart data
setDonutDataKelompokUmur([
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
]);
// Process data for bar chart (group by month)
const monthYearMap = new Map<string, number>();
data.forEach((item: any) => {
// Try both createdAt and tanggal fields
const dateValue = item.tanggal || item.createdAt;
if (!dateValue) return;
const parsedDate = new Date(dateValue);
if (isNaN(parsedDate.getTime())) return;
const month = parsedDate.getMonth() + 1;
const year = parsedDate.getFullYear();
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
});
// Convert map to array and sort by date
const barData = Array.from(monthYearMap.entries())
.map(([key, count]) => {
const [year, month] = key.split('-');
const monthName = new Date(Number(year), Number(month) - 1, 1)
.toLocaleString('id-ID', { month: 'long' });
return {
month: `${monthName} ${year}`,
count,
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
};
})
.sort((a, b) => a.sortKey - b.sortKey)
.map(({ month, count }) => ({ month, count }));
setBarChartData(barData);
}
}, [data]);
if ((loading && !data) || !data) {
return (
<Stack py={10} px="xl">
<Skeleton height={300} mb="md" />
<SimpleGrid cols={{ base: 1, md: 3 }}>
<Skeleton height={300} />
<Skeleton height={300} />
<Skeleton height={300} />
</SimpleGrid>
</Stack>
);
}
if (data.length === 0) {
return (
<Stack p="sm">
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<Center>
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
</Center>
<Center mt={10}>
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
</Center>
</Container>
<Box px={"xl"}>
<Paper p={"lg"} bg={colors.Bg}>
<Paper p={"lg"}>
<Stack gap={"xs"}>
<Flex justify={"space-between"} align={"center"}>
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<BarChart
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
</Paper>
)}
</Stack>
</Paper>
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Pilihan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
</Paper>
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
</Paper>
</SimpleGrid>
</Box>
</Paper>
</Box>
{/* Modal */}
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/>
<Select
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Modal>
</Stack>
);
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack p={"sm"}>
<Box px={{ base: 'md', md: 100 }}> <Container w={{ base: "100%", md: "80%" }} p={"xl"}>
<BackButton /> <Stack gap={"xs"}>
</Box> <Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
<Box> <Group justify={"center"}>
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
Indeks Kepuasan Masyarakat (IKM) </Group>
</Text> </Stack>
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> </Container>
Desa Darmasaba <Box px={"xl"}>
</Text> <Paper p={"lg"} bg={colors.Bg}>
</Box> <Paper p={"lg"}>
<Box px={{ base: "md", md: 100 }}> <Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={'xl'}> <Flex justify={"space-between"} align={"center"}>
<GrafikHasilKepuasan /> <Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
<Box>
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
{state.findMany.total.toLocaleString('id-ID')}
</Text>
</Box>
</Flex>
<BarChart
h={300}
data={barChartData}
dataKey="month"
series={[{ name: 'count', color: colors['blue-button'] }]}
tickLine="y"
xAxisLabel="Bulan"
yAxisLabel="Jumlah Responden"
withTooltip
tooltipAnimationDuration={200}
/>
</Stack>
</Paper>
<Box py={"xl"}>
<SimpleGrid
cols={{
base: 1,
md: 1,
lg: 1,
xl: 3
}}
>
{/* Chart Jenis Kelamin */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Jenis Kelamin</Title>
{donutDataJenisKelamin.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withLabels
withTooltip
labelsType="percent"
size={200}
data={donutDataJenisKelamin}
/>
</Center>
</Box>
<Stack gap="sm" mt="md">
{donutDataJenisKelamin.map((entry) => (
<Flex key={entry.name} gap="md" align="center">
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
<Text size="sm">{entry.name}: {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
</Paper>
)}
</Stack>
</Paper>
{/* Chart Rating */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Pilihan</Title>
{donutDataRating.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={200}
data={donutDataRating}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataRating.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
</Paper>
{/* Chart Kelompok Umur */}
<Paper bg={colors['white-1']} p="md" radius="md">
<Stack>
<Title order={4}>Umur</Title>
{donutDataKelompokUmur.every(item => item.value === 0) ? (
<Text c="dimmed" ta="center" my="md">
Belum ada data untuk ditampilkan dalam grafik
</Text>
) : (
<Paper p="md" radius="md" withBorder>
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Box style={{ position: 'relative', width: '100%' }}>
<Center>
<PieChart
withTooltip
tooltipAnimationDuration={200}
withLabels
labelsPosition="outside"
labelsType="percent"
withLabelsLine
size={190}
data={donutDataKelompokUmur}
/>
</Center>
</Box>
<Box mt="md" style={{ width: '100%' }}>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
{donutDataKelompokUmur.map((entry) => (
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{entry.name}: {entry.value}
</Text>
</Flex>
))}
</SimpleGrid>
</Box>
</Box>
</Paper>
)}
</Stack>
</Paper>
</SimpleGrid>
</Box>
</Paper> </Paper>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> {/* Modal */}
<Paper p={"xl"} bg={colors['white-trans-1']}> <Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
<GrafikBerdasarkanJenisKelamin/> <Paper bg={colors['white-1']} p={'md'}>
<Stack>
<TextInput
label="Nama"
type='text'
placeholder="masukkan nama"
value={state.create.form.name}
onChange={(val) => {
state.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
value={state.create.form.tanggal}
onChange={(val) => {
state.create.form.tanggal = val.currentTarget.value;
}}
/>
<Select
key={"jenisKelamin"}
label={"Jenis Kelamin"}
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
value={state.create.form.jenisKelaminId || ""}
onChange={(val) => {
state.create.form.jenisKelaminId = val ?? "";
}}
data={
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
/>
<Select
key={"rating_responden"}
label={"Rating"}
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
value={state.create.form.ratingId || ""}
onChange={(val) => {
state.create.form.ratingId = val ?? "";
}}
data={
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
/>
<Select
key={"kelompokUmur"}
label={"Kelompok Umur"}
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
value={state.create.form.kelompokUmurId || ""}
onChange={(val) => {
state.create.form.kelompokUmurId = val ?? "";
}}
data={
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
.filter(Boolean) // Hapus null, undefined, dll
.map((item) => ({
value: item.id,
label: item.name || 'Tanpa Nama',
}))
}
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper> </Paper>
</Box> </Modal>
<Box px={{ base: "md", md: 100 }}>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<GrafikBerdasarkanResponden/>
</Paper>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<GrafikBerdasarakanUmur/>
</Paper>
</Box>
</Stack> </Stack>
); );
} }
export default Page; export default Kepuasan;

View File

@@ -197,8 +197,8 @@ const navbarListMenu = [
}, },
{ {
id: "5.5", id: "5.5",
name: "Jumlah Pengangguran 2024-2025", name: "Jumlah Pengangguran",
href: "/darmasaba/ekonomi/jumlah-pengangguran-2024-2025" href: "/darmasaba/ekonomi/jumlah-pengangguran"
}, },
{ {
id: "5.6", id: "5.6",
@@ -207,8 +207,8 @@ const navbarListMenu = [
}, },
{ {
id: "5.7", id: "5.7",
name: "Jumlah Penduduk Miskin 2024-2025", name: "Jumlah Penduduk Miskin",
href: "/darmasaba/ekonomi/jumlah-penduduk-miskin-2024-2025" href: "/darmasaba/ekonomi/jumlah-penduduk-miskin"
}, },
{ {
id: "5.8", id: "5.8",