Compare commits

...

11 Commits

163 changed files with 9334 additions and 1474 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "desa-darmasaba",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"scripts": {
"dev": "next dev --turbopack",

View File

@@ -0,0 +1,51 @@
[
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": null
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 90,
"uneducatedUnemployment": 65,
"percentageChange": -3.1
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -3.2
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 85,
"uneducatedUnemployment": 63,
"percentageChange": -1.3
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 82,
"uneducatedUnemployment": 63,
"percentageChange": -2.0
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 80,
"uneducatedUnemployment": 60,
"percentageChange": -3.4
}
]

View File

@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "DetailDataPengangguran" (
"id" UUID NOT NULL,
"month" VARCHAR(20) NOT NULL,
"year" INTEGER NOT NULL,
"totalUnemployment" INTEGER NOT NULL,
"educatedUnemployment" INTEGER NOT NULL,
"uneducatedUnemployment" INTEGER NOT NULL,
"percentageChange" DOUBLE PRECISION,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "DetailDataPengangguran_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DetailDataPengangguran_month_year_key" ON "DetailDataPengangguran"("month", "year");

View File

@@ -0,0 +1,133 @@
-- CreateTable
CREATE TABLE "ApbDesa" (
"id" TEXT NOT NULL,
"tahun" INTEGER NOT NULL,
"pendapatanId" TEXT NOT NULL,
"belanjaId" TEXT NOT NULL,
"pembiayaanId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ApbDesa_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pendapatan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" INTEGER NOT NULL,
CONSTRAINT "Pendapatan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Belanja" (
"id" TEXT NOT NULL,
"penyelenggaraan" INTEGER NOT NULL,
"pelaksanaanPembangunan" INTEGER NOT NULL,
"pembinaanMasyarakat" INTEGER NOT NULL,
"pemberdayaanMasyarakat" INTEGER NOT NULL,
"penanggulanganBencana" INTEGER NOT NULL,
"total" INTEGER NOT NULL,
CONSTRAINT "Belanja_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pembiayaan" (
"id" TEXT NOT NULL,
"silpa" INTEGER NOT NULL,
CONSTRAINT "Pembiayaan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KlasifikasiBelanja" (
"id" TEXT NOT NULL,
"jenis" TEXT NOT NULL,
"persen" DOUBLE PRECISION NOT NULL,
"total" INTEGER NOT NULL,
"apbDesaId" TEXT NOT NULL,
CONSTRAINT "KlasifikasiBelanja_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RincianBelanja" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"jumlah" INTEGER NOT NULL,
"klasifikasiBelanjaId" TEXT NOT NULL,
CONSTRAINT "RincianBelanja_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KegiatanSubak" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"jumlah" INTEGER NOT NULL,
"apbDesaId" TEXT NOT NULL,
CONSTRAINT "KegiatanSubak_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ApbDesaToKegiatanSubak" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_ApbDesaToKegiatanSubak_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_BelanjaToKlasifikasiBelanja" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_BelanjaToKlasifikasiBelanja_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_KlasifikasiBelanjaToRincianBelanja" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_ApbDesaToKegiatanSubak_B_index" ON "_ApbDesaToKegiatanSubak"("B");
-- CreateIndex
CREATE INDEX "_BelanjaToKlasifikasiBelanja_B_index" ON "_BelanjaToKlasifikasiBelanja"("B");
-- CreateIndex
CREATE INDEX "_KlasifikasiBelanjaToRincianBelanja_B_index" ON "_KlasifikasiBelanjaToRincianBelanja"("B");
-- AddForeignKey
ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_pendapatanId_fkey" FOREIGN KEY ("pendapatanId") REFERENCES "Pendapatan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_belanjaId_fkey" FOREIGN KEY ("belanjaId") REFERENCES "Belanja"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApbDesa" ADD CONSTRAINT "ApbDesa_pembiayaanId_fkey" FOREIGN KEY ("pembiayaanId") REFERENCES "Pembiayaan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaToKegiatanSubak" ADD CONSTRAINT "_ApbDesaToKegiatanSubak_A_fkey" FOREIGN KEY ("A") REFERENCES "ApbDesa"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ApbDesaToKegiatanSubak" ADD CONSTRAINT "_ApbDesaToKegiatanSubak_B_fkey" FOREIGN KEY ("B") REFERENCES "KegiatanSubak"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_BelanjaToKlasifikasiBelanja" ADD CONSTRAINT "_BelanjaToKlasifikasiBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "Belanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_BelanjaToKlasifikasiBelanja" ADD CONSTRAINT "_BelanjaToKlasifikasiBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "KlasifikasiBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" ADD CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_A_fkey" FOREIGN KEY ("A") REFERENCES "KlasifikasiBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_KlasifikasiBelanjaToRincianBelanja" ADD CONSTRAINT "_KlasifikasiBelanjaToRincianBelanja_B_fkey" FOREIGN KEY ("B") REFERENCES "RincianBelanja"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,34 @@
/*
Warnings:
- You are about to drop the column `pelaksanaanPembangunan` on the `Belanja` table. All the data in the column will be lost.
- You are about to drop the column `pemberdayaanMasyarakat` on the `Belanja` table. All the data in the column will be lost.
- You are about to drop the column `pembinaanMasyarakat` on the `Belanja` table. All the data in the column will be lost.
- You are about to drop the column `penanggulanganBencana` on the `Belanja` table. All the data in the column will be lost.
- You are about to drop the column `penyelenggaraan` on the `Belanja` table. All the data in the column will be lost.
- Added the required column `name` to the `Belanja` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Belanja` table without a default value. This is not possible if the table is not empty.
- Added the required column `value` to the `Belanja` table without a default value. This is not possible if the table is not empty.
- Added the required column `total` to the `Pendapatan` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Pendapatan` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Belanja" DROP COLUMN "pelaksanaanPembangunan",
DROP COLUMN "pemberdayaanMasyarakat",
DROP COLUMN "pembinaanMasyarakat",
DROP COLUMN "penanggulanganBencana",
DROP COLUMN "penyelenggaraan",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "name" TEXT NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "value" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "Pendapatan" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "total" INTEGER NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `total` on the `Belanja` table. All the data in the column will be lost.
- You are about to drop the column `total` on the `Pendapatan` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Belanja" DROP COLUMN "total";
-- AlterTable
ALTER TABLE "Pendapatan" DROP COLUMN "total";

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `silpa` on the `Pembiayaan` table. All the data in the column will be lost.
- Added the required column `name` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
- Added the required column `value` to the `Pembiayaan` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Pembiayaan" DROP COLUMN "silpa",
ADD COLUMN "name" TEXT NOT NULL,
ADD COLUMN "value" INTEGER NOT NULL;

View File

@@ -90,6 +90,10 @@ model FileStorage {
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
}
//========================================= MENU PPID ========================================= //
@@ -150,7 +154,7 @@ model DaftarInformasiPublik {
id String @id @default(cuid())
jenisInformasi String
deskripsi String
tanggal String
tanggal DateTime @db.Date
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
@@ -1262,3 +1266,109 @@ model DataDemografiPekerjaan {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= JUMLAH PENGANGGURAN ========================================= //
model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20)
year Int
totalUnemployment Int
educatedUnemployment Int
uneducatedUnemployment Int
percentageChange Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
@@unique([month, year])
}
// ========================================= PADESA PENDAPATAN ASLI DESA ========================================= //
model ApbDesa {
id String @id @default(uuid())
tahun Int
pembiayaan Pembiayaan[] @relation("ApbDesaPembiayaan")
belanja Belanja[] @relation("ApbDesaBelanja")
pendapatan Pendapatan[] @relation("ApbDesaPendapatan")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Pendapatan {
id String @id @default(uuid())
name String
value Int
ApbDesa ApbDesa[] @relation("ApbDesaPendapatan")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model Belanja {
id String @id @default(uuid())
name String
value Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ApbDesa ApbDesa[] @relation("ApbDesaBelanja")
}
model Pembiayaan {
id String @id @default(uuid())
name String
value Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
ApbDesa ApbDesa[] @relation("ApbDesaPembiayaan")
}
// ========================================= INOVASI ========================================= //
// ========================================= DESA DIGITAL / SMART VILLAGE ========================================= //
model DesaDigital {
id String @id @default(cuid())
name String
deskripsi String @db.Text
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PROGRAM KREATIF ========================================= //
model ProgramKreatif {
id String @id @default(cuid())
name String
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KOLABORASI INOVASI ========================================= //
model KolaborasiInovasi {
id String @id @default(cuid())
name String
tahun Int
slug String @db.Text //deskripsi singkat
deskripsi String @db.Text //deskripsi panjang
kolaborator String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -21,6 +21,7 @@ import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
import detailDataPengangguran from './data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json';
(async () => {
for (const l of layanan) {
@@ -431,6 +432,29 @@ import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
});
}
console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
month_year: { month: d.month, year: d.year },
},
update: {
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
create: {
month: d.month,
year: d.year,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
});
}
console.log("📊 detailDataPengangguran success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

BIN
public/pa-desa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -19,7 +19,7 @@ const HeaderSearch = ({
onChange,
}: HeaderSearchProps) => {
return (
<Grid>
<Grid mb={10}>
<GridCol span={{ base: 12, md: 9 }}>
<Title order={3}>{title}</Title>
</GridCol>

View File

@@ -15,6 +15,8 @@ type JudulListTabProps = {
}
const JudulListTab = ({
title = "",
href = "#",

View File

@@ -80,13 +80,33 @@ const berita = proxy({
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.berita["find-many"].get();
if (res.status === 200) {
berita.findMany.data = (res.data?.data ) ?? [];
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
berita.findMany.loading = true;
berita.findMany.page = page;
try {
const res = await ApiFetch.api.desa.berita["find-many"].get({
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) {
berita.findMany.data = res.data.data ?? [];
berita.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
} finally {
berita.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| Prisma.BeritaGetPayload<{

View File

@@ -0,0 +1,887 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateApbDesa = z.object({
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
pembiayaanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pembiayaan"),
belanjaIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 belanja"),
pendapatanIds: z.array(z.string().uuid()).nonempty("Pilih minimal 1 pendapatan"),
});
const ApbDesaDefaultForm = {
tahun: 0,
pendapatanIds: [] as string[],
belanjaIds: [] as string[],
pembiayaanIds: [] as string[],
};
const ApbDesa = proxy({
create: {
form: { ...ApbDesaDefaultForm },
loading: false,
async submit() {
const cek = templateApbDesa.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"create"
].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan APB Desa");
ApbDesa.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan APB Desa");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Gagal menambahkan APB Desa");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...ApbDesaDefaultForm };
},
},
findMany: {
data: null as
| Prisma.ApbDesaGetPayload<{
include: {
pendapatan: true;
belanja: true;
pembiayaan: true;
};
}>[]
| null,
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.apbdesa[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
} else {
toast.error(res.data?.message || "Gagal mengambil APB Desa");
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil APB Desa");
} finally {
this.loading = false;
}
},
},
update: {
id: "",
form: { ...ApbDesaDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error("Gagal mengambil APB Desa");
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "Gagal memuat APB Desa");
}
const data = result.data;
this.id = id;
this.form = {
tahun: data.tahun || 0,
pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
};
return data;
} catch (error) {
console.error("Error loading APB Desa:", error);
toast.error("Gagal memuat data APB Desa");
return null;
}
},
async update() {
try {
this.loading = true;
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/apbdesa/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
if (!response.ok) {
throw new Error("Gagal memperbarui APB Desa");
}
const data = await response.json();
toast.success("APB Desa berhasil diperbarui");
return data;
} catch (error) {
console.error("Error updating APB Desa:", error);
toast.error("Gagal memperbarui APB Desa");
throw error;
} finally {
this.loading = false;
}
},
reset() {
this.id = "";
this.form = { ...ApbDesaDefaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
try {
this.loading = true;
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/apbdesa/del/${id}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error("Gagal menghapus APB Desa");
}
toast.success("APB Desa berhasil dihapus");
return true;
} catch (error) {
console.error("Error deleting APB Desa:", error);
toast.error("Gagal menghapus APB Desa");
return false;
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ApbDesaGetPayload<{
include: { pendapatan: true; belanja: true; pembiayaan: true };
}> | null,
async load(id: string) {
try {
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/apbdesa/${id}`
);
if (!response.ok) {
throw new Error("Gagal mengambil detail APB Desa");
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "Gagal mengambil data");
}
this.data = result.data; // ✅ fix utama di sini
return result.data;
} catch (error) {
console.error("Error loading APB Desa detail:", error);
toast.error("Gagal memuat detail APB Desa");
return null;
}
},
},
});
const templatePendapatan = z.object({
name: z.string().min(2, "Nama harus diisi"),
value: z.number().int().positive("Nilai harus angka positif"),
});
const PendapatanDefaultForm = {
name: "",
value: 0,
};
const pendapatan = proxy({
create: {
form: { ...PendapatanDefaultForm },
loading: false,
async submit() {
const cek = templatePendapatan.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res =
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
"create"
].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan Pendapatan Asli");
pendapatan.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan Pendapatan Asli");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Gagal menambahkan Pendapatan Asli");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...PendapatanDefaultForm };
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
pendapatan.findMany.loading = true; // Use the full path to access the property
pendapatan.findMany.page = page;
try {
const res =
await ApiFetch.api.ekonomi.pendapatanaslidesa.pendapatanasli[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pendapatan.findMany.data = res.data.data || [];
pendapatan.findMany.total = res.data.total || 0;
pendapatan.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load pendapatan:", res.data?.message);
pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pendapatan:", error);
pendapatan.findMany.data = [];
pendapatan.findMany.total = 0;
pendapatan.findMany.totalPages = 1;
} finally {
pendapatan.findMany.loading = false;
}
},
},
update: {
id: "",
form: { ...PendapatanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
value: data.value,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pendapatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePendapatan.safeParse(pendapatan.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pendapatan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pendapatan");
await pendapatan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pendapatan");
}
} catch (error) {
console.error("Error updating pendapatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pendapatan"
);
return false;
} finally {
pendapatan.update.loading = false;
}
},
reset() {
pendapatan.update.id = "";
pendapatan.update.form = { ...PendapatanDefaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pendapatan.delete.loading = true;
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Pendapatan Asli berhasil dihapus");
await pendapatan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Pendapatan Asli");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Pendapatan Asli");
} finally {
pendapatan.delete.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PendapatanGetPayload<{
select: { isActive: boolean };
}> | null,
async load(id: string) {
const res = await fetch(
`/api/ekonomi/pendapatanaslidesa/pendapatanasli/${id}`
);
if (res.ok) {
const json = await res.json();
pendapatan.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
}
: null;
} else {
pendapatan.findUnique.data = null;
}
},
},
});
const templateBelanja = z.object({
name: z.string().min(2, "Nama harus diisi"),
value: z.number().int().positive("Nilai harus angka positif"),
});
const BelanjaDefaultForm = {
name: "",
value: 0,
};
const belanja = proxy({
create: {
form: { ...BelanjaDefaultForm },
loading: false,
async submit() {
const cek = templateBelanja.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
"create"
].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan Belanja");
belanja.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan Belanja");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Gagal menambahkan Belanja");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...BelanjaDefaultForm };
},
},
findMany: {
data: [] as Array<{
id: string;
name: string;
value: number;
}>,
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.belanja[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
} else {
toast.error(res.data?.message || "Gagal mengambil Belanja");
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil Belanja");
} finally {
this.loading = false;
}
},
},
update: {
id: "",
form: { ...BelanjaDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
value: data.value,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading belanja:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateBelanja.safeParse(belanja.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
belanja.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update belanja");
await belanja.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate belanja");
}
} catch (error) {
console.error("Error updating belanja:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate belanja"
);
return false;
} finally {
belanja.update.loading = false;
}
},
reset() {
belanja.update.id = "";
belanja.update.form = { ...BelanjaDefaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
belanja.delete.loading = true;
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/belanja/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Belanja berhasil dihapus");
await belanja.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Belanja");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Belanja");
} finally {
belanja.delete.loading = false;
}
},
},
findUnique: {
data: null as Prisma.BelanjaGetPayload<{
select: { isActive: boolean };
}> | null,
async load(id: string) {
const res = await fetch(`/api/ekonomi/pendapatanaslidesa/belanja/${id}`);
if (res.ok) {
const json = await res.json();
belanja.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
}
: null;
} else {
belanja.findUnique.data = null;
}
},
},
});
const templatePembiayaan = z.object({
name: z.string().min(2, "Nama harus diisi"),
value: z.number().int().positive("Nilai harus angka positif"),
});
const PembiayaanDefaultForm = {
name: "",
value: 0,
};
const pembiayaan = proxy({
create: {
form: { ...PembiayaanDefaultForm },
loading: false,
async submit() {
const cek = templatePembiayaan.safeParse(this.form);
if (!cek.success) {
const err = cek.error.issues.map((v) => v.message).join("\n");
return toast.error(err);
}
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
"create"
].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan Pembiayaan");
pembiayaan.findMany.load();
this.reset();
} else {
toast.error(res.data?.message || "Gagal menambahkan Pembiayaan");
}
} catch (error) {
console.error("Create error:", error);
toast.error("Gagal menambahkan Pembiayaan");
} finally {
this.loading = false;
}
},
reset() {
this.form = { ...PembiayaanDefaultForm };
},
},
findMany: {
data: [] as Array<{
id: string;
name: string;
value: number;
}>,
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi.pendapatanaslidesa.pembiayaan[
"find-many"
].get();
if (res.status === 200) {
this.data = res.data?.data ?? [];
} else {
toast.error(res.data?.message || "Gagal mengambil Pembiayaan");
}
} catch (error) {
console.error("Find many error:", error);
toast.error("Gagal mengambil Pembiayaan");
} finally {
this.loading = false;
}
},
},
update: {
id: "",
form: { ...PembiayaanDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
value: data.value,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pembiayaan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templatePembiayaan.safeParse(pembiayaan.update.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pembiayaan.update.loading = true;
const response = await fetch(`/api/ekonomi/pendapatanaslidesa/pembiayaan/${this.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
value: this.form.value,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pembiayaan");
await pembiayaan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate pembiayaan");
}
} catch (error) {
console.error("Error updating pembiayaan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengupdate pembiayaan"
);
return false;
} finally {
pembiayaan.update.loading = false;
}
},
reset() {
pembiayaan.update.id = "";
pembiayaan.update.form = { ...PembiayaanDefaultForm };
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pembiayaan.delete.loading = true;
const response = await fetch(
`/api/ekonomi/pendapatanaslidesa/pembiayaan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Pembiayaan berhasil dihapus");
await pembiayaan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Pembiayaan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Pembiayaan");
} finally {
pembiayaan.delete.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PembiayaanGetPayload<{
select: { isActive: boolean };
}> | null,
async load(id: string) {
const res = await fetch(
`/api/ekonomi/pendapatanaslidesa/pembiayaan/${id}`
);
if (res.ok) {
const json = await res.json();
pembiayaan.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? true, // Fallback ke aktif:true jika tidak ada data
}
: null;
} else {
pembiayaan.findUnique.data = null;
}
},
},
});
const PendapatanAsliDesa = proxy({
ApbDesa,
belanja,
pembiayaan,
pendapatan,
});
export default PendapatanAsliDesa;

View File

@@ -0,0 +1,243 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateJumlahPengngguran = z.object({
month: z.string().min(1, "Bulan harus diisi"),
year: z.number().min(1, "Tahun harus diisi"),
totalUnemployment: z.number().min(1, "Total pengangguran harus diisi"),
educatedUnemployment: z
.number()
.min(1, "Pengangguran pendidikan harus diisi"),
uneducatedUnemployment: z
.number()
.min(1, "Pengangguran tidak pendidikan harus diisi"),
percentageChange: z.number().min(0, "Persentase perubahan harus diisi"),
});
type JumlahPengangguran = {
month: string;
year: number;
totalUnemployment: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
};
const jumlahPengangguranForm: JumlahPengangguran = {
month: "",
year: 0,
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
percentageChange: 0,
};
const jumlahPengangguran = proxy({
findByMonthYear: {
data: null as any,
loading: false,
load: async ({ month, year }: { month: string; year: number }) => {
jumlahPengangguran.findByMonthYear.loading = true;
try {
const res = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/month/${month}/year/${year}`
);
const json = await res.json();
jumlahPengangguran.findByMonthYear.data = json.data;
return json.data;
} catch (err) {
console.error("Gagal ambil data bulan/tahun:", err);
} finally {
jumlahPengangguran.findByMonthYear.loading = false;
}
},
},
create: {
form: jumlahPengangguranForm,
loading: false,
async create() {
const cek = templateJumlahPengngguran.safeParse(
jumlahPengangguran.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
jumlahPengangguran.create.loading = true;
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"create"
].post(jumlahPengangguran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
jumlahPengangguran.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
jumlahPengangguran.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DetailDataPengangguranGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get();
if (res.status === 200) {
jumlahPengangguran.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DetailDataPengangguranGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/${id}`
);
if (res.ok) {
const data = await res.json();
jumlahPengangguran.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch jumlahPengangguran:", res.statusText);
jumlahPengangguran.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching jumlahPengangguran:", error);
jumlahPengangguran.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...jumlahPengangguranForm },
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
month: this.form.month,
year: this.form.year,
totalUnemployment: this.form.totalUnemployment,
educatedUnemployment: this.form.educatedUnemployment,
uneducatedUnemployment: this.form.uneducatedUnemployment,
percentageChange: this.form.percentageChange,
};
const cek = templateJumlahPengngguran.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
}
);
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPengangguran.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data jumlah pengangguran");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPengangguran.delete.loading = true;
const response = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Jumlah pengangguran berhasil dihapus"
);
await jumlahPengangguran.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus jumlah pengangguran");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jumlah pengangguran");
} finally {
jumlahPengangguran.delete.loading = false;
}
},
},
});
const jumlahPengangguranState = proxy({
jumlahPengangguran,
});
export default jumlahPengangguranState;

View File

@@ -30,9 +30,9 @@ const posisiOrganisasi = proxy({
try {
this.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["create"].post(
this.form
);
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["create"].post(this.form);
if (res.status === 200) {
toast.success("Berhasil menambahkan posisi organisasi");
posisiOrganisasi.findMany.load();
@@ -62,12 +62,15 @@ const posisiOrganisasi = proxy({
return null;
}
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const response = await fetch(
`/api/ekonomi/struktur-organisasi/posisi-organisasi/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -160,7 +163,9 @@ const posisiOrganisasi = proxy({
}>,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["posisi-organisasi"]["find-many"].get();
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"posisi-organisasi"
]["find-many"].get();
if (res.status === 200) {
// The API now returns the id field, so we can use it directly
this.data = res.data?.data ?? [];
@@ -209,238 +214,278 @@ const posisiOrganisasi = proxy({
});
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
const pegawai = proxy({
create: {
form: { ...pegawaiDefaultForm },
loading: false,
async submit() {
const cek = templatePegawai.safeParse(pegawai.create.form);
if (!cek.success) {
const err = cek.error.issues.map(i => i.message).join("\n");
toast.error(err);
return;
}
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["create"].post(
pegawai.create.form
);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah pegawai");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
pegawai.create.loading = false;
}
},
},
findMany: {
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean })[] | null,
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["find-many"].get();
if (res.status === 200) {
pegawai.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
posisi: item.posisi || { id: '', nama: '' }, // Ensure posisi exists with required fields
isActive: item.isActive ?? true // Default to true if not provided
}));
} else {
console.error('Failed to load pegawai:', res.data?.message);
}
} catch (error) {
console.error('Error loading pegawai:', error);
pegawai.findMany.data = [];
}
},
},
findUnique: {
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean }) | null,
async load(id: string) {
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data ? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true // Fallback ke aktif:true jika tidak ada data
} : null;
} else {
pegawai.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
method: "DELETE",
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
await pegawai.findMany.load();
} else {
toast.error(json.message ?? "Gagal hapus pegawai");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
pegawai.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update pegawai");
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
const pegawai = proxy({
create: {
form: { ...pegawaiDefaultForm },
loading: false,
async submit() {
const cek = templatePegawai.safeParse(pegawai.create.form);
if (!cek.success) {
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
pegawai.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["create"].post(pegawai.create.form);
if (res.status === 200) {
toast.success("Pegawai berhasil ditambahkan");
await pegawai.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah pegawai");
}
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
pegawai.create.loading = false;
}
},
},
// In struktur-organisasi.ts
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
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;
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"pegawai"
]["find-many"].get({
query: { page, limit },
});
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.total = 0;
pegawai.findMany.totalPages = 1;
} finally {
pegawai.findMany.loading = false;
}
},
},
findUnique: {
data: null as
| (Prisma.PegawaiGetPayload<{
include: { posisi: true; image: true };
}> & { isActive: boolean })
| null,
async load(id: string) {
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data
? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true, // Fallback ke aktif:true jika tidak ada data
}
: null;
} else {
pegawai.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`,
{
method: "DELETE",
}
);
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil hapus pegawai");
await pegawai.findMany.load();
} else {
toast.error(json.message ?? "Gagal hapus pegawai");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus");
} finally {
pegawai.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(
`/api/ekonomi/struktur-organisasi/pegawai/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pegawai"
);
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
// Schema Zod untuk form validasi
const templateHubunganOrganisasiForm = z.object({
@@ -474,7 +519,9 @@ const hubunganOrganisasi = proxy({
try {
hubunganOrganisasi.create.loading = true;
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["create"].post(hubunganOrganisasi.create.form);
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["create"].post(hubunganOrganisasi.create.form);
if (res.status === 200 && res.data?.success) {
hubunganOrganisasi.findMany.load();
@@ -482,7 +529,7 @@ const hubunganOrganisasi = proxy({
} else {
return toast.error(res.data?.message || "Gagal menambahkan data");
}
} catch (error) {
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan");
} finally {
@@ -490,7 +537,7 @@ const hubunganOrganisasi = proxy({
}
},
},
findMany: {
findMany: {
data: null as Array<{
id: string;
atasanId: string;
@@ -528,20 +575,29 @@ const hubunganOrganisasi = proxy({
async load() {
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["find-many"].get();
const res = await ApiFetch.api.ekonomi["struktur-organisasi"][
"hubungan-organisasi"
]["find-many"].get();
if (res.status === 200) {
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
atasan: item.atasan ? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true
} : null,
bawahan: item.bawahan ? {
...item.bawahan,
isActive: item.bawahan.isActive ?? item.bawahan.aktif ?? true
} : null
}));
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map(
(item: any) => ({
...item,
atasan: item.atasan
? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true,
}
: null,
bawahan: item.bawahan
? {
...item.bawahan,
isActive:
item.bawahan.isActive ?? item.bawahan.aktif ?? true,
}
: null,
})
);
} else {
hubunganOrganisasi.findMany.data = [];
}
@@ -591,7 +647,9 @@ const hubunganOrganisasi = proxy({
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`);
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
@@ -616,7 +674,9 @@ const hubunganOrganisasi = proxy({
if (!id) return toast.warn("ID tidak valid");
try {
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`);
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/${id}`
);
const result = await res.json();
if (res.ok && result?.success) {
@@ -633,7 +693,9 @@ const hubunganOrganisasi = proxy({
}
} catch (error) {
console.error("Error loading:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
@@ -690,9 +752,12 @@ const hubunganOrganisasi = proxy({
try {
hubunganOrganisasi.delete.loading = true;
const res = await fetch(`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`, {
method: "DELETE",
});
const res = await fetch(
`/api/ekonomi/struktur-organisasi/hubungan-organisasi/del/${id}`,
{
method: "DELETE",
}
);
const result = await res.json();
if (res.ok && result?.success) {

View File

@@ -0,0 +1,216 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1).max(50),
deskripsi: z.string().min(1).max(5000),
imageId: z.string().min(1).max(50),
});
const defaultForm = {
name: "",
deskripsi: "",
imageId: "",
};
const desaDigitalState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(desaDigitalState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
desaDigitalState.create.loading = true;
const res = await ApiFetch.api.inovasi.desadigital["create"].post(
desaDigitalState.create.form
);
if (res.status === 200) {
desaDigitalState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
desaDigitalState.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DesaDigitalGetPayload<{
include: {
image: true;
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.inovasi.desadigital["find-many"].get();
if (res.status === 200) {
desaDigitalState.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DesaDigitalGetPayload<{
include: {
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/desadigital/${id}`);
if (res.ok) {
const data = await res.json();
desaDigitalState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
desaDigitalState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading desa digital:", error);
desaDigitalState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
desaDigitalState.delete.loading = true;
const response = await fetch(`/api/inovasi/desadigital/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok) {
toast.success(result.message || "Desa Digital berhasil dihapus");
await desaDigitalState.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus desa digital");
}
} catch (error) {
console.log((error as Error).message);
toast.error("Terjadi kesalahan saat menghapus desa digital");
} finally {
desaDigitalState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/inovasi/desadigital/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading desa digital:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(desaDigitalState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
desaDigitalState.edit.loading = true;
const response = await fetch(
`/api/inovasi/desadigital/${desaDigitalState.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
deskripsi: this.form.deskripsi,
imageId: this.form.imageId,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update desa digital");
await desaDigitalState.findMany.load();
return true;
} else {
throw new Error(result?.message || "Gagal update desa digital");
}
} catch (error) {
console.error("Error updating desa digital:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update desa digital"
);
return false;
} finally {
desaDigitalState.edit.loading = false;
}
},
reset() {
desaDigitalState.edit.id = "";
desaDigitalState.edit.form = { ...defaultForm };
},
},
});
export default desaDigitalState;

View File

@@ -0,0 +1,234 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
tahun: z.number().min(4, "Tahun minimal 4 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
kolaborator: z.string().min(1, "Kolaborator minimal 1 karakter"),
imageId: z.string().min(1, "Image ID minimal 1 karakter"),
})
const defaultForm = {
name: "",
tahun: 0,
slug: "",
deskripsi: "",
kolaborator: "",
imageId: "",
}
const kolaborasiInovasiState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(kolaborasiInovasiState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
kolaborasiInovasiState.create.loading = true;
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["create"].post(
kolaborasiInovasiState.create.form
);
if (res.status === 200) {
kolaborasiInovasiState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
kolaborasiInovasiState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
kolaborasiInovasiState.findMany.loading = true; // Use the full path to access the property
kolaborasiInovasiState.findMany.page = page;
try {
const res = await ApiFetch.api.inovasi.kolaborasiinovasi["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
kolaborasiInovasiState.findMany.data = res.data.data || [];
kolaborasiInovasiState.findMany.total = res.data.total || 0;
kolaborasiInovasiState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
kolaborasiInovasiState.findMany.data = [];
kolaborasiInovasiState.findMany.total = 0;
kolaborasiInovasiState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
kolaborasiInovasiState.findMany.data = [];
kolaborasiInovasiState.findMany.total = 0;
kolaborasiInovasiState.findMany.totalPages = 1;
} finally {
kolaborasiInovasiState.findMany.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
tahun: data.tahun,
slug: data.slug,
deskripsi: data.deskripsi,
kolaborator: data.kolaborator,
imageId: data.imageId,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading kolaborasi inovasi:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await kolaborasiInovasiState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data kolaborasi inovasi");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KolaborasiInovasiGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/kolaborasiinovasi/${id}`);
if (res.ok) {
const data = await res.json();
kolaborasiInovasiState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
kolaborasiInovasiState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading kolaborasi inovasi:", error);
kolaborasiInovasiState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
kolaborasiInovasiState.delete.loading = true;
const response = await fetch(`/api/inovasi/kolaborasiinovasi/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Kolaborasi inovasi berhasil dihapus");
await kolaborasiInovasiState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus kolaborasi inovasi");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus kolaborasi inovasi");
} finally {
kolaborasiInovasiState.delete.loading = false;
}
},
},
});
export default kolaborasiInovasiState;

View File

@@ -0,0 +1,227 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
slug: z.string().min(1, "Deskripsi singkat minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
slug: "",
icon: "",
};
const programKreatifState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programKreatifState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programKreatifState.create.loading = true;
const res = await ApiFetch.api.inovasi.programkreatif["create"].post(
programKreatifState.create.form
);
if (res.status === 200) {
programKreatifState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
programKreatifState.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => {
// Change to arrow function
programKreatifState.findMany.loading = true; // Use the full path to access the property
programKreatifState.findMany.page = page;
try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || [];
programKreatifState.findMany.total = res.data.total || 0;
programKreatifState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1;
} finally {
programKreatifState.findMany.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
deskripsi: data.deskripsi,
slug: data.slug,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/inovasi/programkreatif/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await programKreatifState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data program kreatif");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramKreatifGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/inovasi/programkreatif/${id}`);
if (res.ok) {
const data = await res.json();
programKreatifState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programKreatifState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading program kreatif:", error);
programKreatifState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programKreatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/programkreatif/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program kreatif berhasil dihapus");
await programKreatifState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program kreatif");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program kreatif");
} finally {
programKreatifState.delete.loading = false;
}
},
},
});
export default programKreatifState;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -16,17 +17,9 @@ const defaultForm = {
tanggal: "",
};
type DaftarInformasi = Prisma.DaftarInformasiPublikGetPayload<{
select: {
jenisInformasi: true;
deskripsi: true;
tanggal: true;
};
}>;
const daftarInformasiPublik = proxy({
create: {
form: {} as DaftarInformasi,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateDaftarInformasi.safeParse(
@@ -56,15 +49,38 @@ const daftarInformasiPublik = proxy({
},
},
findMany: {
data: null as
| Prisma.DaftarInformasiPublikGetPayload<{ omit: { isActive: true } }>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get();
if (res.status === 200) {
daftarInformasiPublik.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
daftarInformasiPublik.findMany.loading = true; // Use the full path to access the property
daftarInformasiPublik.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.daftarinformasipublik[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
daftarInformasiPublik.findMany.data = res.data.data || [];
daftarInformasiPublik.findMany.total = res.data.total || 0;
daftarInformasiPublik.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load daftar informasi publik:", res.data?.message);
daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading daftar informasi publik:", error);
daftarInformasiPublik.findMany.data = [];
daftarInformasiPublik.findMany.total = 0;
daftarInformasiPublik.findMany.totalPages = 1;
} finally {
daftarInformasiPublik.findMany.loading = false;
}
},
},
@@ -186,7 +202,9 @@ const daftarInformasiPublik = proxy({
}
try {
daftarInformasiPublik.edit.loading = true;
const formattedTanggal = this.form.tanggal
? new Date(this.form.tanggal).toISOString()
: undefined;
const response = await fetch(
`/api/ppid/daftarinformasipublik/${this.id}`,
{
@@ -197,7 +215,7 @@ const daftarInformasiPublik = proxy({
body: JSON.stringify({
jenisInformasi: this.form.jenisInformasi,
deskripsi: this.form.deskripsi,
tanggal: this.form.tanggal,
tanggal: formattedTanggal,
}),
}
);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -9,71 +10,75 @@ const templateGrafikJenisKelamin = z.object({
perempuan: z.string().min(1, "Data perempuan harus diisi"),
});
type GrafikJenisKelamin = Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
select: {
id: true;
laki: true;
perempuan: true;
};
}>;
const defaultForm: Omit<GrafikJenisKelamin, 'id'> & { id?: string } = {
const defaultForm = {
laki: "",
perempuan: "",
};
const grafikBerdasarkanJenisKelamin = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikJenisKelamin.safeParse(
grafikBerdasarkanJenisKelamin.create.form
);
async create(){
const cek = templateGrafikJenisKelamin.safeParse(grafikBerdasarkanJenisKelamin.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
const err = cek.error.issues.map((i) => i.message).join("\n");
toast.error(err);
return;
}
try {
grafikBerdasarkanJenisKelamin.create.loading = true;
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"create"
].post(grafikBerdasarkanJenisKelamin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanJenisKelamin.create.form = {
laki: "",
perempuan: "",
};
grafikBerdasarkanJenisKelamin.findMany.load();
return id;
}
toast.success("Grafik berdasarkan jenis kelamin berhasil ditambahkan");
await grafikBerdasarkanJenisKelamin.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan jenis kelamin");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan jenis kelamin");
} finally {
grafikBerdasarkanJenisKelamin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanJenisKelaminGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanJenisKelamin.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanJenisKelamin.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanjeniskelamin[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanJenisKelamin.findMany.data = res.data.data || [];
grafikBerdasarkanJenisKelamin.findMany.total = res.data.total || 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan jenis kelamin:", res.data?.message);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error);
grafikBerdasarkanJenisKelamin.findMany.data = [];
grafikBerdasarkanJenisKelamin.findMany.total = 0;
grafikBerdasarkanJenisKelamin.findMany.totalPages = 1;
} finally {
grafikBerdasarkanJenisKelamin.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,17 +12,7 @@ const templateGrafikResponden = z.object({
tidakbaik: z.string().min(1, "Data tidak baik harus diisi"),
});
type GrafikResponden = Prisma.GrafikBerdasarkanRespondenGetPayload<{
select: {
id: true;
sangatbaik: true;
baik: true;
kurangbaik: true;
tidakbaik: true;
};
}>;
const defaultForm: Omit<GrafikResponden, 'id'> & { id?: string } = {
const defaultForm = {
sangatbaik: "",
baik: "",
kurangbaik: "",
@@ -30,7 +21,7 @@ const defaultForm: Omit<GrafikResponden, 'id'> & { id?: string } = {
const grafikBerdasarkanResponden = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikResponden.safeParse(
@@ -48,40 +39,52 @@ const grafikBerdasarkanResponden = proxy({
"create"
].post(grafikBerdasarkanResponden.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikBerdasarkanResponden.create.form = {
sangatbaik: "",
baik: "",
kurangbaik: "",
tidakbaik: "",
};
grafikBerdasarkanResponden.findMany.load();
return id;
}
toast.success("Grafik berdasarkan responden berhasil ditambahkan");
await grafikBerdasarkanResponden.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik berdasarkan responden");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik berdasarkan responden");
} finally {
grafikBerdasarkanResponden.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanRespondenGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanResponden.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanResponden.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanResponden.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanresponden[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanResponden.findMany.data = res.data.data || [];
grafikBerdasarkanResponden.findMany.total = res.data.total || 0;
grafikBerdasarkanResponden.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan responden:", res.data?.message);
grafikBerdasarkanResponden.findMany.data = [];
grafikBerdasarkanResponden.findMany.total = 0;
grafikBerdasarkanResponden.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafikBerdasarkanResponden:", error);
grafikBerdasarkanResponden.findMany.data = [];
grafikBerdasarkanResponden.findMany.total = 0;
grafikBerdasarkanResponden.findMany.totalPages = 1;
} finally {
grafikBerdasarkanResponden.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,17 +12,7 @@ const templateGrafikUmur = z.object({
lansia: z.string().min(1, "Data lansia harus diisi"),
});
type GrafikUmur = Prisma.GrafikBerdasarkanUmurGetPayload<{
select: {
id: true;
remaja: true;
dewasa: true;
orangtua: true;
lansia: true;
};
}>;
const defaultForm: Omit<GrafikUmur, "id"> & { id?: string } = {
const defaultForm = {
remaja: "",
dewasa: "",
orangtua: "",
@@ -30,7 +21,7 @@ const defaultForm: Omit<GrafikUmur, "id"> & { id?: string } = {
const grafikBerdasarkanUmur = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikUmur.safeParse(
@@ -70,18 +61,38 @@ const grafikBerdasarkanUmur = proxy({
},
},
findMany: {
data: null as
| Prisma.GrafikBerdasarkanUmurGetPayload<{
omit: { isActive: true };
}>[]
| null,
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
async load() {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get();
if (res.status === 200) {
grafikBerdasarkanUmur.findMany.data = res.data?.data ?? [];
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikBerdasarkanUmur.findMany.loading = true; // Use the full path to access the property
grafikBerdasarkanUmur.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikberdasarkanumur[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUmur.findMany.data = res.data.data || [];
grafikBerdasarkanUmur.findMany.total = res.data.total || 0;
grafikBerdasarkanUmur.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik berdasarkan umur:", res.data?.message);
grafikBerdasarkanUmur.findMany.data = [];
grafikBerdasarkanUmur.findMany.total = 0;
grafikBerdasarkanUmur.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan umur:", error);
grafikBerdasarkanUmur.findMany.data = [];
grafikBerdasarkanUmur.findMany.total = 0;
grafikBerdasarkanUmur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUmur.findMany.loading = false;
}
},
},

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -9,22 +10,14 @@ const templateGrafikHasilKepuasanMasyarakat = z.object({
kepuasan: z.string().min(1, "Kepuasan harus diisi"),
});
type GrafikHasilKepuasanMasyarakat = Prisma.IndeksKepuasanMasyarakatGetPayload<{
select: {
id: true;
label: true;
kepuasan: true;
};
}>;
const defaultForm: Omit<GrafikHasilKepuasanMasyarakat, 'id'> & { id?: string } = {
const defaultForm = {
label: "",
kepuasan: "",
};
const grafikHasilKepuasanMasyarakat = proxy({
create: {
form: defaultForm,
form: {...defaultForm},
loading: false,
async create() {
const cek = templateGrafikHasilKepuasanMasyarakat.safeParse(
@@ -38,42 +31,52 @@ const grafikHasilKepuasanMasyarakat = proxy({
}
try {
grafikHasilKepuasanMasyarakat.create.loading = true;
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["create"].post(
grafikHasilKepuasanMasyarakat.create.form
);
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["create"].post(grafikHasilKepuasanMasyarakat.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
grafikHasilKepuasanMasyarakat.create.form = {
label: "",
kepuasan: "",
};
grafikHasilKepuasanMasyarakat.findMany.load();
return id;
}
toast.success("Grafik hasil kepuasan masyarakat berhasil ditambahkan");
await grafikHasilKepuasanMasyarakat.findMany.load();
} else {
toast.error(res.data?.message ?? "Gagal tambah grafik hasil kepuasan masyarakat");
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan grafik hasil kepuasan masyarakat");
} finally {
grafikHasilKepuasanMasyarakat.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.IndeksKepuasanMasyarakatGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat[
"find-many"
].get();
if (res.status === 200) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data?.data ?? [];
data: null as any[] | null,
page: 1,
totalPages: 1,
total: 0,
loading: false,
load: async (page = 1, limit = 10) => { // Change to arrow function
grafikHasilKepuasanMasyarakat.findMany.loading = true; // Use the full path to access the property
grafikHasilKepuasanMasyarakat.findMany.page = page;
try {
const res = await ApiFetch.api.ppid.grafikhasilkepuasamanmasyarakat["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
grafikHasilKepuasanMasyarakat.findMany.data = res.data.data || [];
grafikHasilKepuasanMasyarakat.findMany.total = res.data.total || 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error("Failed to load grafik hasil kepuasan masyarakat:", res.data?.message);
grafikHasilKepuasanMasyarakat.findMany.data = [];
grafikHasilKepuasanMasyarakat.findMany.total = 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik hasil kepuasan masyarakat:", error);
grafikHasilKepuasanMasyarakat.findMany.data = [];
grafikHasilKepuasanMasyarakat.findMany.total = 0;
grafikHasilKepuasanMasyarakat.findMany.totalPages = 1;
} finally {
grafikHasilKepuasanMasyarakat.findMany.loading = false;
}
},
},

View File

@@ -18,8 +18,6 @@ import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
import EditEditor from "@/app/admin/(dashboard)/_com/editEditor";
import colors from "@/con/colors";
import ApiFetch from "@/lib/api-fetch";

View File

@@ -1,6 +1,6 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { Box, Button, Center, Grid, GridCol, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -29,12 +29,21 @@ function Berita() {
function ListBerita({ search }: { search: string }) {
const beritaState = useProxy(stateDashboardBerita)
const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = beritaState.berita.findMany;
// Fetch pertama kali
useShallowEffect(() => {
beritaState.berita.findMany.load()
}, [])
load(page, 10); // awal page = 1
}, [page]);
const filteredData = (beritaState.berita.findMany.data || []).filter(item => {
const filteredData = (data || []).filter((item) => {
const keyword = search.toLowerCase();
return (
item.judul.toLowerCase().includes(keyword) ||
@@ -42,67 +51,85 @@ function ListBerita({ search }: { search: string }) {
);
});
if (!beritaState.berita.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
if (loading || !data) {
return <Skeleton h={500} />;
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors["white-1"]} p={"md"}>
<Stack>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>List Berita</Text>
<Text fz={"xl"} fw={"bold"}>
List Berita
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push("/admin/desa/berita/create")} bg={colors['blue-button']}>
<Button
onClick={() => router.push("/admin/desa/berita/create")}
bg={colors["blue-button"]}
>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<Table
striped
withRowBorders
withTableBorder
style={{ minWidth: "700px" }}
>
<TableThead>
<TableTr>
<TableTh w={250}>Judul</TableTh>
<TableTh w={250}>Kategori</TableTh>
<TableTh w={250}>Image</TableTh>
<TableTh w={200}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody >
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd >
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{item.judul}</Text>
<Text truncate="end" fz={"sm"}>
{item.judul}
</Text>
</Box>
</TableTd>
<TableTd >{item.kategoriBerita?.name}</TableTd>
<TableTd>{item.kategoriBerita?.name}</TableTd>
<TableTd>
<Image w={100} src={item.image?.link} alt="gambar" />
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/desa/berita/${item.id}`)}>
<Button
bg={"green"}
onClick={() =>
router.push(`/admin/desa/berita/${item.id}`)
}
>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table> </Box>
</Table>
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
);
}
export default Berita;

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';

View File

@@ -1,5 +1,5 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';

View File

@@ -1,5 +1,5 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';

View File

@@ -6,7 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../_com/jusulListTab';
import JudulListTab from '../../_com/judulListTab';
import { useState } from 'react';
import HeaderSearch from '../../_com/header';

View File

@@ -0,0 +1,73 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "APB Desa",
value: "apbdesa",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
},
{
label: "Pendapatan",
value: "pendapatan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan"
},
{
label: "Belanja",
value: "belanja",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja"
},
{
label: "Pembiayaan",
value: "pembiayaan",
href: "/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Pendapatan Asli Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,234 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
tahun: apbState.update.form.tahun || '',
pendapatanIds: apbState.update.form.pendapatanIds || [],
belanjaIds: apbState.update.form.belanjaIds || [],
pembiayaanIds: apbState.update.form.pembiayaanIds || [],
});
// Load APB desa by id saat pertama kali
useEffect(() => {
const loadAPBdesa = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await apbState.update.load(id);
if (data) {
setFormData({
tahun: data.tahun || 0,
pendapatanIds: data.pendapatan?.map((p: any) => p.id) || [],
belanjaIds: data.belanja?.map((b: any) => b.id) || [],
pembiayaanIds: data.pembiayaan?.map((p: any) => p.id) || [],
});
}
} catch (error) {
console.error("Error loading APBdesa:", error);
toast.error("Gagal memuat data APBdesa");
}
};
loadAPBdesa();
}, [params?.id]); // ✅ hapus beritaState dari dependency
const handleSubmit = async () => {
try {
// Update global state with form data
apbState.update.form = {
...apbState.update.form,
tahun: Number(formData.tahun),
pendapatanIds: formData.pendapatanIds,
belanjaIds: formData.belanjaIds,
pembiayaanIds: formData.pembiayaanIds,
};
await apbState.update.update();
toast.success("APB Desa berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa");
} catch (error) {
console.error("Error updating APBdesa:", error);
toast.error("Terjadi kesalahan saat memperbarui APBdesa");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit APB Desa</Title>
<TextInput
type='number'
value={formData.tahun}
onChange={(val) => {
setFormData({ ...formData, tahun: val.target.value });
}}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
/>
<SelectPendapatan
selectedIds={formData.pendapatanIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, pendapatanIds: ids });
}}
/>
<SelectBelanja
selectedIds={formData.belanjaIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, belanjaIds: ids });
}}
/>
<SelectPembiayaan
selectedIds={formData.pembiayaanIds}
onSelectionChange={(ids) => {
setFormData({ ...formData, pembiayaanIds: ids });
}}
/>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
/* Select Pendapatan */
interface SelectPendapatanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
pendapatanState.findMany.load().then(() => {
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
}, []);
if (!pendapatanState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({
value: p.id,
label: p.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pendapatan..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
/* Select Belanja */
interface SelectBelanjaProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
belanjaState.findMany.load().then(() => {
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
}, []);
if (!belanjaState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({
value: b.id,
label: b.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih belanja..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
/* Select Pembiayaan */
interface SelectPembiayaanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => {
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
}, []);
if (!pembiayaanState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({
value: b.id,
label: b.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pembiayaan..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
}
export default EditAPBDesa;

View File

@@ -0,0 +1,152 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailAPBDesa() {
const apbState = useProxy(PendapatanAsliDesa.ApbDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
console.log("PARAM ID:", params?.id)
apbState.findUnique.load(params?.id as string)
}, [])
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const handleHapus = () => {
if (selectedId) {
apbState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
}
}
if (!apbState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail APB Desa</Text>
{apbState.findUnique.data ? (
<Paper key={apbState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Tahun</Text>
<Text fz={"lg"}>{apbState.findUnique.data?.tahun}</Text>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Pembiayaan</Text>
{(apbState.findUnique.data?.pembiayaan || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.pembiayaan || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
</Text>
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Belanja</Text>
{(apbState.findUnique.data?.belanja || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.belanja || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
</Text>
</Stack>
</Box>
<Box>
<Stack gap={"xs"}>
<Text fw={"bold"} fz={"lg"}>Detail Pendapatan</Text>
{(apbState.findUnique.data?.pendapatan || []).map((item) => (
<Text fz={"lg"} key={item.id}>
{item.name}: {formatRupiah(Number(item.value))}
</Text>
))}
<Text fz={"lg"} fw={"bold"}>
Total: {formatRupiah((apbState.findUnique.data?.pendapatan || [])
.reduce((sum, item) => sum + Number(item.value), 0))}
</Text>
</Stack>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (apbState.findUnique.data) {
setSelectedId(apbState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={apbState.delete.loading || !apbState.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (apbState.findUnique.data) {
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${apbState.findUnique.data.id}/edit`);
}
}}
disabled={!apbState.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus APB Desa ini?'
/>
</Box>
);
}
export default DetailAPBDesa;

View File

@@ -0,0 +1,190 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, MultiSelect, Paper, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateAPBDesa() {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const router = useRouter()
const resetForm = () => {
apbDesaState.create.form = {
tahun: 0,
pendapatanIds: [],
belanjaIds: [],
pembiayaanIds: [],
}
}
const handleSubmit = async () => {
await apbDesaState.create.submit()
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create APB Desa</Title>
<TextInput
type='number'
value={apbDesaState.create.form.tahun}
onChange={(val) => {
apbDesaState.create.form.tahun = Number(val.target.value);
}}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>}
placeholder="masukkan tahun"
/>
<SelectPendapatan
selectedIds={apbDesaState.create.form.pendapatanIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.pendapatanIds = ids;
}}
/>
<SelectBelanja
selectedIds={apbDesaState.create.form.belanjaIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.belanjaIds = ids;
}}
/>
<SelectPembiayaan
selectedIds={apbDesaState.create.form.pembiayaanIds}
onSelectionChange={(ids) => {
apbDesaState.create.form.pembiayaanIds = ids;
}}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
/* Select Pendapatan */
interface SelectPendapatanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPendapatan({
selectedIds = [],
onSelectionChange,
}: SelectPendapatanProps) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
useShallowEffect(() => {
pendapatanState.findMany.load().then(() => {
console.log("Pendapatan berhasil dimuat:", pendapatanState.findMany.data);
});
}, []);
if (!pendapatanState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pendapatan</Text>}
data={pendapatanState.findMany.data.map(p => ({
value: p.id,
label: p.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pendapatan..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
/* Select Belanja */
interface SelectBelanjaProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectBelanja({
selectedIds = [],
onSelectionChange,
}: SelectBelanjaProps) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
useShallowEffect(() => {
belanjaState.findMany.load().then(() => {
console.log("Belanja berhasil dimuat:", belanjaState.findMany.data);
});
}, []);
if (!belanjaState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Belanja</Text>}
data={belanjaState.findMany.data.map(b => ({
value: b.id,
label: b.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih belanja..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
/* Select Pembiayaan */
interface SelectPembiayaanProps {
selectedIds: string[];
onSelectionChange: (ids: string[]) => void;
}
function SelectPembiayaan({
selectedIds = [],
onSelectionChange,
}: SelectPembiayaanProps) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
useShallowEffect(() => {
pembiayaanState.findMany.load().then(() => {
console.log("Pembiayaan berhasil dimuat:", pembiayaanState.findMany.data);
});
}, []);
if (!pembiayaanState.findMany.data) {
return <Skeleton height={38} />;
}
return (
<MultiSelect
label={<Text fz={"sm"} fw={"bold"}>Pembiayaan</Text>}
data={pembiayaanState.findMany.data.map(b => ({
value: b.id,
label: b.name
}))}
value={selectedIds}
onChange={onSelectionChange}
searchable
clearable
placeholder="Pilih pembiayaan..."
nothingFoundMessage="Tidak ditemukan"
/>
);
}
}
export default CreateAPBDesa;

View File

@@ -0,0 +1,106 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
function APBDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='APB Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListAPBDesa search={search} />
</Box>
);
}
function ListAPBDesa({ search }: { search: string }) {
const apbDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
const router = useRouter();
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
useShallowEffect(() => {
apbDesaState.findMany.load();
}, [])
const filteredData = (apbDesaState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.tahun.toString().toLowerCase().includes(keyword) ||
item.pembiayaan.map((item) => item.value.toString()).includes(keyword) ||
item.belanja.map((item) => item.value.toString()).includes(keyword) ||
item.pendapatan.map((item) => item.value.toString()).includes(keyword)
);
});
if (!apbDesaState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List APB Desa'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Tahun</TableTh>
<TableTh>Pembiayaan</TableTh>
<TableTh>Belanja</TableTh>
<TableTh>Pendapatan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.tahun}</TableTd>
<TableTd>{formatRupiah(item.pembiayaan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>{formatRupiah(item.belanja.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>{formatRupiah(item.pendapatan.reduce((sum, item) => sum + Number(item.value), 0))}</TableTd>
<TableTd>
<Button
bg={"green"}
onClick={() =>
router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa/${item.id}`)
}
>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default APBDesa;

View File

@@ -0,0 +1,112 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: belanjaState.update.form.name || '',
value: belanjaState.update.form.value || '',
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
useEffect(() => {
const loadBelanja = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await belanjaState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
});
}
} catch (error) {
console.error("Error loading belanja:", error);
toast.error("Gagal memuat data belanja");
}
};
loadBelanja();
}, [params?.id]);
const handleSubmit = async () => {
try {
belanjaState.update.form = {
...belanjaState.update.form,
name: formData.name,
value: Number(formData.value),
}
await belanjaState.update.update();
toast.success("Jenis Belanja berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja");
} catch (error) {
console.error("Error updating jenis belanja:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis belanja");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pendapatan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama Jenis Pendapatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditBelanja;

View File

@@ -0,0 +1,77 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const router = useRouter()
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
const resetForm = () => {
belanjaState.create.form = {
name: "",
value: 0,
}
}
const handleSubmit = async () => {
await belanjaState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Belanja</Title>
<TextInput
value={belanjaState.create.form.name}
onChange={(val) => {
belanjaState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Belanja</Text>}
placeholder='Masukkan nama jenis belanja'
/>
<TextInput
type='text'
value={formatRupiah(belanjaState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
belanjaState.create.form.value = cleanValue;
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateBelanja;

View File

@@ -0,0 +1,139 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Belanja() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Belanja'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBelanja search={search} />
</Box>
);
}
function ListBelanja({ search }: { search: string }) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const totalBelanja = belanjaState.findMany.data.reduce((sum, item) => sum + item.value, 0);
const handleDelete = () => {
if (selectedId) {
belanjaState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
belanjaState.findMany.load()
}
}
useShallowEffect(() => {
belanjaState.findMany.load();
}, [])
const filteredData = (belanjaState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!belanjaState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Belanja'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>{((item.value / totalBelanja) * 100).toFixed(0)}%</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus belanja ini?'
/>
</Box>
);
}
export default Belanja;

View File

@@ -0,0 +1,12 @@
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
PADesa-pendapatan-asli-desa
</div>
);
}
export default Page;

View File

@@ -0,0 +1,112 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: pembiayaanState.update.form.name || '',
value: pembiayaanState.update.form.value || '',
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
useEffect(() => {
const loadPembiayaan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pembiayaanState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
});
}
} catch (error) {
console.error("Error loading pembiayaan:", error);
toast.error("Gagal memuat data pembiayaan");
}
};
loadPembiayaan();
}, [params?.id]);
const handleSubmit = async () => {
try {
pembiayaanState.update.form = {
...pembiayaanState.update.form,
name: formData.name,
value: Number(formData.value),
}
await pembiayaanState.update.update();
toast.success("Jenis Pembiayaan berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan");
} catch (error) {
console.error("Error updating jenis pembiayaan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis pembiayaan");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pembiayaan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
placeholder='Masukkan nama Jenis Pembiayaan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPembiayaan;

View File

@@ -0,0 +1,78 @@
'use client'
import React from 'react';
import { useProxy } from 'valtio/utils';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import { useRouter } from 'next/navigation';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreatePembiayaan() {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
const router = useRouter()
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
const resetForm = () => {
pembiayaanState.create.form = {
name: "",
value: 0,
}
}
const handleSubmit = async () => {
await pembiayaanState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pembiayaan</Title>
<TextInput
value={pembiayaanState.create.form.name}
onChange={(val) => {
pembiayaanState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pembiayaan</Text>}
placeholder='Masukkan nama jenis pembiayaan'
/>
<TextInput
type='text'
value={formatRupiah(pembiayaanState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
pembiayaanState.create.form.value = cleanValue;
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePembiayaan;

View File

@@ -0,0 +1,138 @@
'use client'
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React, { useState } from 'react';
import HeaderSearch from '../../../_com/header';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import { useProxy } from 'valtio/utils';
import { useRouter } from 'next/navigation';
import { useShallowEffect } from '@mantine/hooks';
import colors from '@/con/colors';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function Pembiayaan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pembiayaan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPembiayaan search={search} />
</Box>
);
}
function ListPembiayaan({ search }: { search: string }) {
const pembiayaanState = useProxy(PendapatanAsliDesa.pembiayaan)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const totalPembiayaan = pembiayaanState.findMany.data.reduce((sum, item) => sum + item.value, 0);
const handleDelete = () => {
if (selectedId) {
pembiayaanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
pembiayaanState.findMany.load()
}
}
useShallowEffect(() => {
pembiayaanState.findMany.load();
}, [])
const filteredData = (pembiayaanState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!pembiayaanState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pembiayaan'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>{((item.value / totalPembiayaan) * 100).toFixed(0)}%</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pembiayaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={pembiayaanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(pembiayaanState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pembiayaan ini?'
/>
</Box>
)
}
export default Pembiayaan;

View File

@@ -0,0 +1,112 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan);
const router = useRouter();
const params = useParams();
const [formData, setFormData] = useState({
name: pendapatanState.update.form.name || '',
value: pendapatanState.update.form.value || '',
});
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
useEffect(() => {
const loadPendapatan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await pendapatanState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
value: data.value || '',
});
}
} catch (error) {
console.error("Error loading pendapatan:", error);
toast.error("Gagal memuat data pendapatan");
}
};
loadPendapatan();
}, [params?.id]);
const handleSubmit = async () => {
try {
pendapatanState.update.form = {
...pendapatanState.update.form,
name: formData.name,
value: Number(formData.value),
}
await pendapatanState.update.update();
toast.success("Jenis Pendapatan berhasil diperbarui!");
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan");
} catch (error) {
console.error("Error updating jenis pendapatan:", error);
toast.error("Terjadi kesalahan saat memperbarui jenis pendapatan");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pendapatan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama Jenis Pendapatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPendapatan;

View File

@@ -0,0 +1,77 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreatePendapatan() {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
const router = useRouter()
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(number);
};
const unformatRupiah = (value: string) => {
return Number(value.replace(/\D/g, ''));
};
const resetForm = () => {
pendapatanState.create.form = {
name: "",
value: 0,
}
}
const handleSubmit = async () => {
await pendapatanState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Pendapatan</Title>
<TextInput
value={pendapatanState.create.form.name}
onChange={(val) => {
pendapatanState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama jenis pendapatan'
/>
<TextInput
type='text'
value={formatRupiah(pendapatanState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
pendapatanState.create.form.value = cleanValue;
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePendapatan;

View File

@@ -0,0 +1,136 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Pendapatan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pendapatan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPendapatan search={search} />
</Box>
);
}
function ListPendapatan({ search }: { search: string }) {
const pendapatanState = useProxy(PendapatanAsliDesa.pendapatan)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const handleDelete = () => {
if (selectedId) {
pendapatanState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
pendapatanState.findMany.load()
}
}
useShallowEffect(() => {
pendapatanState.findMany.load();
}, [])
const filteredData = (pendapatanState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
if (!pendapatanState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pendapatan'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/pendapatan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={pendapatanState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(pendapatanState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus pendapatan ini?'
/>
</Box>
);
}
export default Pendapatan;

View File

@@ -1,11 +1,11 @@
'use client'
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
@@ -40,8 +40,6 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
const stateDemografi = useProxy(demografiPekerjaan)
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -54,7 +52,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
stateDemografi.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateDemografi.findMany.load()
@@ -104,7 +102,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/${item.id}`)}>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
@@ -138,14 +136,18 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="pekerjaan" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="lakiLaki" fill="#f03e3e" name="Laki - Laki" />
<Bar dataKey="perempuan" fill="#ff922b" name="Perempuan" />
</BarChart>
<Box w={{ base: '100%', md: '30%' }}>
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: 'red.6', label: 'Laki - Laki' },
{ name: 'perempuan', color: 'orange.6', label: 'Perempuan' },
]}
/>
</Box>
)}
</Paper>
</Box>

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';

View File

@@ -1,45 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function CreateJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jumlah Pengangguran</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Terdidik</Text>}
placeholder='Masukkan pengangguran terdidik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia Produktif</Text>}
placeholder='Masukkan usia produktif'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Sedang Mencari Kerja</Text>}
placeholder='Masukkan sedang mencari kerja'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Tidak Terdidik</Text>}
placeholder='Masukkan pengangguran tidak terdidik'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPengangguran;

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Jumlah Pengangguran</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text>
<Text>100</Text>
</Box>
<Box>
<Text fw={"bold"}>Usia Produktif</Text>
<Text>200</Text>
</Box>
<Box>
<Text fw={"bold"}>Sedang Mencari Kerja</Text>
<Text>300</Text>
</Box>
<Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text>
<Text>30</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran-2024-2025/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailJumlahPengangguran;

View File

@@ -1,45 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jumlah Pengangguran</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Terdidik</Text>}
placeholder='Masukkan pengangguran terdidik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia Produktif</Text>}
placeholder='Masukkan usia produktif'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Sedang Mencari Kerja</Text>}
placeholder='Masukkan sedang mencari kerja'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Tidak Terdidik</Text>}
placeholder='Masukkan pengangguran tidak terdidik'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditJumlahPengangguran;

View File

@@ -1,58 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function JumlahPengangguran() {
return (
<Box>
<HeaderSearch
title='Jumlah Pengangguran 2024-2025'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListJumlahPengangguran/>
</Box>
);
}
function ListJumlahPengangguran() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Pengangguran 2024-2025'
href='/admin/ekonomi/jumlah-pengangguran-2024-2025/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pengangguran Terdidik</TableTh>
<TableTh>Usia Produktif</TableTh>
<TableTh>Sedang Mencari Kerja</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>100</TableTd>
<TableTd>200</TableTd>
<TableTd>300</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran-2024-2025/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default JumlahPengangguran;

View File

@@ -0,0 +1,185 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter();
const params = useParams()
const [formData, setFormData] = useState({
month: stateDetail.update.form.month,
year: stateDetail.update.form.year,
totalUnemployment: stateDetail.update.form.totalUnemployment,
educatedUnemployment: stateDetail.update.form.educatedUnemployment,
uneducatedUnemployment: stateDetail.update.form.uneducatedUnemployment,
percentageChange: stateDetail.update.form.percentageChange || 0, // Ensure it's always a number
})
const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// Ambil data bulan sebelumnya
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === formData.month.toLowerCase()
);
let percentageChange = 0;
if (currentIndex > 0) {
const prevMonth = monthOrder[currentIndex - 1];
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: formData.year,
});
if (prev?.totalUnemployment) {
percentageChange = Number(
(((total - prev.totalUnemployment) / prev.totalUnemployment) * 100).toFixed(1)
);
}
}
setFormData({
...formData,
totalUnemployment: total,
percentageChange,
});
return { total, percentageChange };
};
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateDetail.findUnique.load(id); // ambil by ID
const data = stateDetail.findUnique.data;
if (data) {
// Set the ID for update
stateDetail.update.id = id;
// Isi state Valtio untuk update
stateDetail.update.form = {
...data,
percentageChange: data.percentageChange || 0 // Ensure it's always a number
};
// Isi local formData supaya input bisa dikontrol
setFormData({
month: data.month,
year: data.year,
totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
percentageChange: data.percentageChange || 0, // Ensure it's always a number
});
}
} catch (error) {
console.error("Error loading detail:", error);
toast.error("Gagal memuat data detail");
}
};
loadDetail();
}, [params?.id]);
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange();
try {
stateDetail.update.form = {
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit();
if (success) {
toast.success("Detail data pengangguran berhasil diperbarui!");
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran");
}
} catch (error) {
console.error("Error updating:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Edit Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
label="Bulan"
value={formData.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(val) => (setFormData({
...formData,
month: val.currentTarget.value
}))}
/>
<TextInput
label="Tahun"
type="number"
value={formData.year}
onChange={(val) => (setFormData({
...formData,
year: Number(val.currentTarget.value)
}))}
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
value={formData.educatedUnemployment}
onChange={(val) => (setFormData({
...formData,
educatedUnemployment: Number(val.currentTarget.value)
}))}
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
value={formData.uneducatedUnemployment}
onChange={(val) => (setFormData({
...formData,
uneducatedUnemployment: Number(val.currentTarget.value)
}))}
/>
<Text fz="sm" fw={500}>
Total Otomatis: {formData.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{formData.percentageChange !== null
? `${formData.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditDetailDataPengangguran;

View File

@@ -0,0 +1,111 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailJumlahPengangguran() {
const router = useRouter();
const params = useParams()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran")
}
}
if (!stateDetail.findUnique.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Pengangguran</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text>
<Text>{stateDetail.findUnique.data?.educatedUnemployment}</Text>
</Box>
<Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text>
<Text>{stateDetail.findUnique.data?.uneducatedUnemployment}</Text>
</Box>
<Box>
<Text fw={"bold"}>Perubahan</Text>
<Text>{stateDetail.findUnique.data?.percentageChange}</Text>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text>{stateDetail.findUnique.data?.year}</Text>
</Box>
<Box>
<Text fw={"bold"}>Bulan</Text>
<Text>{stateDetail.findUnique.data?.month}</Text>
</Box>
<Box>
<Text fw={"bold"}>Total Pengangguran</Text>
<Text>{stateDetail.findUnique.data?.totalUnemployment}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?"
/>
</Box>
);
}
export default DetailJumlahPengangguran;

View File

@@ -0,0 +1,139 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter();
const resetForm = () => {
stateDetail.create.form = {
month: "",
year: 0,
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
percentageChange: 0,
}
}
const calculateTotalAndChange = async () => {
const total =
stateDetail.create.form.educatedUnemployment +
stateDetail.create.form.uneducatedUnemployment;
stateDetail.create.form.totalUnemployment = total;
// Ambil data bulan sebelumnya
const monthOrder = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
);
if (currentIndex > 0) {
const prevMonth = monthOrder[currentIndex - 1];
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: stateDetail.create.form.year,
});
if (prev?.totalUnemployment) {
const change = ((total - prev.totalUnemployment) / prev.totalUnemployment) * 100;
stateDetail.create.form.percentageChange = Number(change.toFixed(1));
} else {
stateDetail.create.form.percentageChange = 0;
}
} else {
stateDetail.create.form.percentageChange = 0;
}
};
const handleSubmit = async () => {
await calculateTotalAndChange();
const id = await stateDetail.create.create();
if (id) {
await stateDetail.findUnique.load(String(id));
if (stateDetail.findUnique.data) {
setChartData([stateDetail.findUnique.data]);
}
resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
label="Bulan"
value={stateDetail.create.form.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(e) => (stateDetail.create.form.month = e.currentTarget.value)}
/>
<TextInput
label="Tahun"
type="number"
value={stateDetail.create.form.year}
onChange={(e) =>
(stateDetail.create.form.year = Number(e.currentTarget.value))
}
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
value={stateDetail.create.form.educatedUnemployment}
onChange={(e) => {
stateDetail.create.form.educatedUnemployment = Number(
e.currentTarget.value,
);
}}
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
value={stateDetail.create.form.uneducatedUnemployment}
onChange={(e) => {
stateDetail.create.form.uneducatedUnemployment = Number(
e.currentTarget.value,
);
}}
/>
<Text fz="sm" fw={500}>
Total Otomatis: {stateDetail.create.form.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{stateDetail.create.form.percentageChange !== null
? `${stateDetail.create.form.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPengangguran;

View File

@@ -0,0 +1,148 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
import JudulListTab from '../../_com/judulListTab';
function DetailDataPengangguran() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title>
<ListDetailDataPengangguran />
</Stack>
</Box>
);
}
function ListDetailDataPengangguran() {
type DetailDataPengangguran = {
id: string;
month: string;
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
totalUnemployment: number;
}
const [chartData, setChartData] = useState<DetailDataPengangguran[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter();
const [search, setSearch] = useState("")
useShallowEffect(() => {
setMounted(true)
stateDetail.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateDetail.findMany.data) {
setChartData(stateDetail.findMany.data.map((item) => ({
id: item.id,
month: item.month,
year: item.year,
educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment),
})));
}
}, [stateDetail.findMany.data]);
const filteredData = (stateDetail.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.month.toLowerCase().includes(keyword) ||
item.year.toString().toLowerCase().includes(keyword)
);
});
if (!stateDetail.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"md"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Detail Data Pengangguran'
href='/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/create'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Terdidik</TableTh>
<TableTh>Tidak Terdidik</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.month}</TableTd>
<TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</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 Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
)}
</Paper>
</Box>
)}
</Stack>
</Box>
);
}
export default DetailDataPengangguran;

View File

@@ -1,14 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { 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 { useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
function Pegawai() {
const [search, setSearch] = useState("");
@@ -30,46 +30,33 @@ function ListPegawai({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const router = useRouter();
useShallowEffect(() => {
const loadData = async () => {
try {
// Clear existing data to ensure we see the loading state
stateOrganisasi.findMany.data = [];
const {
data,
page,
totalPages,
loading,
load,
} = stateOrganisasi.findMany;
// Load new data
await stateOrganisasi.findMany.load();
useEffect(() => {
load(page, 10);
}, [page]);
// Type guard to ensure data is an array
const data = stateOrganisasi.findMany.data || [];
if (data.length > 0) {
console.log('4. First record sample:', data[0]);
}
} catch (error) {
console.error('Error loading pegawai data:', error);
stateOrganisasi.findMany.data = [];
}
};
loadData();
// Cleanup function
return () => {
console.log('Cleanup: Unmounting component');
};
}, []);
const filteredData = (stateOrganisasi.findMany.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)
);
});
const filteredData = useMemo(() => {
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)
);
});
}, [data, search]);
// Handle loading state
if (stateOrganisasi.findMany.data === null) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={300} />
@@ -77,8 +64,6 @@ function ListPegawai({ search }: { search: string }) {
);
}
// Check if data is an empty array
const data = stateOrganisasi.findMany.data || [];
if (data.length === 0) {
return (
<Box py={10}>
@@ -90,53 +75,84 @@ function ListPegawai({ search }: { search: string }) {
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Badge color={item.isActive ? "green" : "red"}>{item.isActive ? "Aktif" : "Tidak Aktif"}</Badge>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
<Box style={{ overflowX: "auto" }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{([...filteredData]
.sort((a, b) => {
if (a.isActive === b.isActive) {
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
}
return Number(b.isActive) - Number(a.isActive); // aktif duluan
}) // Aktif di atas
).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Group gap="xs" wrap="nowrap">
<Box visibleFrom="sm">
<Badge color={item.isActive ? "green" : "red"}>
{item.isActive ? "Aktif" : "Tidak Aktif"}
</Badge>
</Box>
<Box hiddenFrom="sm">
{item.isActive ? (
<ThemeIcon color="green" variant="light" size="sm">
<IconCheck size={16} />
</ThemeIcon>
) : (
<ThemeIcon color="red" variant="light" size="sm">
<IconX size={16} />
</ThemeIcon>
)}
</Box>
</Group>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -1,15 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
function PosisiOrganisasi() {
const [search, setSearch] = useState("");
@@ -33,7 +33,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])

View File

@@ -0,0 +1,137 @@
'use client'
/* eslint-disable react-hooks/exhaustive-deps */
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import desaDigitalState from '@/app/admin/(dashboard)/_state/inovasi/desa-digital';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditPenghargaan() {
const stateDesaDigital = useProxy(desaDigitalState)
const router = useRouter()
const params = useParams()
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null)
const [formData, setFormData] = useState({
name: stateDesaDigital.findUnique.data?.name || '',
deskripsi: stateDesaDigital.findUnique.data?.deskripsi || '',
imageId: stateDesaDigital.findUnique.data?.imageId || '',
})
useEffect(() => {
const loadPenghargaan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateDesaDigital.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
});
if (data?.image?.link) {
setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading desa digital smart village:", error);
toast.error("Gagal memuat data desa digital smart village");
}
};
loadPenghargaan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateDesaDigital.edit.form = {
...stateDesaDigital.edit.form,
name: formData.name,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
}
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
stateDesaDigital.edit.form.imageId = uploaded.id;
}
await stateDesaDigital.edit.update();
toast.success("Desa digital smart village berhasil diperbarui!");
router.push("/admin/inovasi/desa-digital-smart-village");
} catch (error) {
console.error("Error updating desa digital smart village:", error);
toast.error("Terjadi kesalahan saat memperbarui desa digital smart village");
}
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={30} />
</Button>
</Box>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Edit Desa Digital Smart Village</Title>
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>}
placeholder="masukkan judul"
/>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Baru (Opsional)</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateDesaDigital.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Button onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditPenghargaan;

View File

@@ -0,0 +1,107 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function DetailDesaDigital() {
const stateDesaDigital = useProxy(desaDigitalState)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
stateDesaDigital.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
stateDesaDigital.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/desa-digital-smart-village")
}
}
if (!stateDesaDigital.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
{stateDesaDigital.findUnique.data ? (
<Paper key={stateDesaDigital.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul</Text>
<Text fz={"lg"}>{stateDesaDigital.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateDesaDigital.findUnique.data?.deskripsi }} />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateDesaDigital.findUnique.data?.image?.link} alt="gambar" />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateDesaDigital.findUnique.data) {
setSelectedId(stateDesaDigital.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDesaDigital.delete.loading || !stateDesaDigital.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateDesaDigital.findUnique.data) {
router.push(`/admin/inovasi/desa-digital-smart-village/${stateDesaDigital.findUnique.data.id}/edit`);
}
}}
disabled={!stateDesaDigital.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus desa digital smart village ini?'
/>
</Box>
);
}
export default DetailDesaDigital;

View File

@@ -1,48 +1,115 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, FileInput, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import desaDigitalState from '../../../_state/inovasi/desa-digital';
function CreateDesaDigital() {
const router = useRouter();
const stateDesaDigital = useProxy(desaDigitalState)
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const router = useRouter()
const resetForm = () => {
stateDesaDigital.create.form = {
name: "",
deskripsi: "",
imageId: "",
}
setPreviewImage(null)
setFile(null)
}
const handleSubmit = async () => {
if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu")
}
try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file,
name: file.name
})
const uploaded = uploadRes.data?.data
if (!uploaded?.id) {
return toast.error("Gagal upload gambar")
}
// Set the image ID in the form
stateDesaDigital.create.form.imageId = uploaded.id
// Submit the form
const success = await stateDesaDigital.create.create()
if (success) {
resetForm()
router.push("/admin/inovasi/desa-digital-smart-village")
}
} catch (error) {
console.error("Error in handleSubmit:", error)
toast.error("Terjadi kesalahan saat menyimpan data")
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={4}>Create Desa Digital Smart Village</Title>
<Title order={3}>Create Desa Digital Smart Village</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Inovasi</Text>}
placeholder='Masukkan nama inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Inovasi</Text>}
placeholder='Masukkan deskripsi singkat inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Image</Text>}
placeholder='Masukkan image'
value={stateDesaDigital.create.form.name}
onChange={(val) => {
stateDesaDigital.create.form.name = val.target.value;
}}
label={<Text fz={"sm"} fw={"bold"}>Nama Desa Digital Smart Village</Text>}
placeholder="masukkan nama desa digital smart village"
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Inovasi</Text>
<KeamananEditor
showSubmit={false}
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<CreateEditor
value={stateDesaDigital.create.form.deskripsi}
onChange={(htmlContent) => {
stateDesaDigital.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
<FileInput
label={<Text fz={"sm"} fw={"bold"}>Upload Gambar Konten</Text>}
value={file}
onChange={async (e) => {
if (!e) return;
setFile(e);
const base64 = await e.arrayBuffer().then((buf) =>
"data:image/png;base64," + Buffer.from(buf).toString("base64")
);
setPreviewImage(base64);
}}
/>
{previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} />
) : (
<Center w={200} h={200} bg={"gray"}>
<IconImageInPicture />
</Center>
)}
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconImageInPicture, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailDesaDigital() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Desa Digital Smart Village</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Nama Inovasi</Text>
<Text>Pelayanan Admin Digital</Text>
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Singkat Inovasi</Text>
<Text>Deskripsi Singkat Inovasi</Text>
</Box>
<Box>
<Text fw={"bold"}>Image</Text>
<IconImageInPicture size={20} />
</Box>
<Box>
<Text fw={"bold"}>Deskripsi Inovasi</Text>
<Text>Deskripsi Inovasi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/desa-digital-smart-village/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailDesaDigital;

View File

@@ -1,49 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditDesaDigital() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Desa Digital Smart Village</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Inovasi</Text>}
placeholder='Masukkan nama inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Inovasi</Text>}
placeholder='Masukkan deskripsi singkat inovasi'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Image</Text>}
placeholder='Masukkan image'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Inovasi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditDesaDigital;

View File

@@ -1,26 +1,53 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, 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 { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import desaDigitalState from '../../_state/inovasi/desa-digital';
function DesaDigitalSmartVillage() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Desa Digital Smart Village'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDesaDigitalSmartVillage/>
<ListDesaDigitalSmartVillage search={search} />
</Box>
);
}
function ListDesaDigitalSmartVillage() {
const router = useRouter();
function ListDesaDigitalSmartVillage({ search }: { search: string }) {
const state = useProxy(desaDigitalState)
const router = useRouter()
useShallowEffect(() => {
state.findMany.load()
}, [])
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -33,25 +60,25 @@ function ListDesaDigitalSmartVillage() {
<TableTr>
<TableTh>Nama Inovasi</TableTh>
<TableTh>Deskripsi Singkat Inovasi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Layanan Admin Digital</TableTd>
<TableTd>Deskripsi Singkat Inovasi</TableTd>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>
<Image src={"/"} alt=''/>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/desa-digital-smart-village/detail')}>
<Button onClick={() => router.push(`/admin/inovasi/desa-digital-smart-village/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
</Box>
);

View File

@@ -1,26 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import kolaborasiInovasiState from '../../_state/inovasi/kolaborasi-inovasi';
import { useProxy } from 'valtio/utils';
function KolaborasiInovasi() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Kolaborasi Inovasi'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKolaborasiInovasi/>
<ListKolaborasiInovasi search={search} />
</Box>
);
}
function ListKolaborasiInovasi() {
function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.kolaborator.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
@@ -31,26 +89,43 @@ function ListKolaborasiInovasi() {
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Kolaborasi Inovasi 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi Singkat</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.name}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>{item.slug}</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}

View File

@@ -3,7 +3,7 @@ import colors from "@/con/colors";
import { Box, Button, Paper, Stack, Title, TextInput, Group, Text } from "@mantine/core";
import { IconArrowBack, IconImageInPicture } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { KeamananEditor } from "../../../keamanan/_com/keamananEditor";
// import { KeamananEditor } from "../../../keamanan/_com/keamananEditor";
export default function EditLayananOnlineDesa() {
const router = useRouter();
@@ -28,9 +28,9 @@ export default function EditLayananOnlineDesa() {
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Layanan Online Desa</Text>
<KeamananEditor
{/* <KeamananEditor
showSubmit={false}
/>
/> */}
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>

View File

@@ -0,0 +1,149 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../_lib/selectIconEdit';
interface FormProgramKreatif {
name: string;
deskripsi: string;
slug: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah';
function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
deskripsi: '',
slug: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateProgramKreatif.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = {
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
slug: data.slug,
deskripsi: data.deskripsi,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramKreatif.update.form = {
...stateProgramKreatif.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(),
icon: formData.icon.trim(),
}
await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa");
} catch (error) {
console.error("Error updating program kreatif:", error);
toast.error("Gagal memuat data program kreatif");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Edit Program Kreatif Desa</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<TextInput
value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="masukkan deskripsi singkat program kreatif desa"
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramKreatif.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramKreatif.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconChartLine, IconEdit, IconLeaf, IconRecycle, IconTent, IconTrophy, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState)
const router = useRouter()
const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null)
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateProgramKreatif.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/program-kreatif-desa")
}
}
if (!stateProgramKreatif.findUnique.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{iconMap[stateProgramKreatif.findUnique.data?.icon] && (
<Box title={stateProgramKreatif.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramKreatif.findUnique.data?.icon], { size: 24 })}
</Box>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
setSelectedId(stateProgramKreatif.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateProgramKreatif.findUnique.data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramKreatif.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?"
/>
</Box>
);
}
export default DetailProgramKreatifDesa;

View File

@@ -0,0 +1,75 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgram(
{ onChange }: { onChange: (value: IconKey) => void }) {
const [selectedIcon, setSelectedIcon] = useState<IconKey>('ekowisata');
const IconComponent = iconMap[selectedIcon]?.icon || null;
// Push default icon ke state saat render awal
useEffect(() => {
onChange(selectedIcon);
}, []);
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={selectedIcon}
onChange={(value) => {
if (value) {
setSelectedIcon(value as IconKey);
onChange(value as IconKey);
}
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -0,0 +1,70 @@
'use client'
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
const iconMap = {
ekowisata: { label: 'Ekowisata', icon: IconLeaf },
kompetisi: { label: 'Kompetisi', icon: IconTrophy },
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({
value,
label: data.label,
}));
export default function SelectIconProgramEdit({
onChange,
value,
}: {
onChange: (value: IconKey) => void;
value: IconKey;
}) {
const IconComponent = iconMap[value]?.icon || null;
return (
<Box maw={300}>
<Select
placeholder="Pilih ikon"
value={value}
onChange={(value) => {
if (value) onChange(value as IconKey);
}}
data={iconList}
leftSection={
IconComponent && (
<Box>
<IconComponent size={24} stroke={1.5} />
</Box>
)
}
withCheckIcon={false}
searchable={false}
rightSectionWidth={0}
styles={{
input: {
textAlign: 'left',
fontSize: rem(16),
paddingLeft: 40,
},
section: {
left: 10,
right: 'auto',
},
}}
/>
</Box>
);
}

View File

@@ -1,48 +1,70 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../_lib/selectIcon';
function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
slug: "",
deskripsi: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/inovasi/program-kreatif-desa")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Program Kreatif Desa</Title>
<Title order={3}>Create Program Kreatif Desa</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama program kreatif desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Stack>
</Paper>
</Box>
</Box>
);
}

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={"/"} alt="gambar" />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text>
<Text fz={"lg"}>Test Deskripsi Singkat</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-desa/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailProgramKreatifDesa;

View File

@@ -1,49 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditProgramKreatifDesa() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Program Kreatif Desa</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Kreatif Desa</Text>}
placeholder='Masukkan nama Program Kreatif Desa'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -1,58 +1,155 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import {
IconChartLine,
IconLeaf,
IconRecycle,
IconTent,
IconTrophy,
} from '@tabler/icons-react';
function ProgramKreatifDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Kreatif Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramKreatifDesa/>
<ListProgramKreatifDesa search={search} />
</Box>
);
}
function ListProgramKreatifDesa() {
function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
};
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={650} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Kreatif Desa'
href='/admin/inovasi/program-kreatif-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Program Kreatif Desa</TableTh>
<TableTh>Image</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>Program Kreatif Desa 1</TableTd>
<TableTd>Image</TableTd>
<TableTd>Deskripsi Singkat</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/inovasi/program-kreatif-desa/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default ProgramKreatifDesa;

View File

@@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import HeaderSearch from '../../../_com/header';

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
function KeteranganBankSampahTerdekat() {
const router = useRouter();

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
function ListPengelolaanSampahBankSampah() {
const router = useRouter();

View File

@@ -10,17 +10,29 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
interface FormDaftarInformasi {
jenisInformasi: string;
deskripsi: string;
tanggal: string;
}
function EditDaftarInformasiPublik() {
const daftarInformasi = useProxy(daftarInformasiPublik)
const router = useRouter()
const params = useParams()
const [formData, setFormData] = useState({
jenisInformasi: daftarInformasi.edit.form.jenisInformasi || '',
deskripsi: daftarInformasi.edit.form.deskripsi || '',
tanggal: daftarInformasi.edit.form.tanggal || '',
const [formData, setFormData] = useState<FormDaftarInformasi>({
jenisInformasi: '',
deskripsi: '',
tanggal: '',
})
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
};
useEffect(() => {
const loadDaftarInformasi = async () => {
const id = params?.id as string;
@@ -48,12 +60,11 @@ function EditDaftarInformasiPublik() {
try {
daftarInformasi.edit.form = {
...daftarInformasi.edit.form,
jenisInformasi: formData.jenisInformasi,
deskripsi: formData.deskripsi,
tanggal: formData.tanggal,
jenisInformasi: formData.jenisInformasi.trim(),
deskripsi: formData.deskripsi.trim(),
tanggal: formData.tanggal.trim(),
}
await daftarInformasi.edit.update()
toast.success("Berita berhasil diperbarui!");
router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba");
} catch (error) {
console.error("Error updating berita:", error);
@@ -73,7 +84,7 @@ function EditDaftarInformasiPublik() {
<Title order={3}>Edit Daftar Informasi Publik Desa Darmasaba</Title>
<TextInput
value={formData.jenisInformasi}
label="Jenis Informasi"
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
setFormData({
@@ -93,8 +104,9 @@ function EditDaftarInformasiPublik() {
/>
</Box>
<TextInput
value={formData.tanggal}
label="Tanggal Publikasi"
type='date'
value={formatDateForInput(formData.tanggal)}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
placeholder="masukkan tanggal publikasi"
onChange={(val) => {
setFormData({

View File

@@ -56,7 +56,9 @@ function DetailDaftarInformasiPublik() {
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.tanggal}</Text>
<Text fz={"lg"}>{stateDaftarInformasi.findUnique.data?.tanggal
? new Date(stateDaftarInformasi.findUnique.data.tanggal).toLocaleDateString()
: "-"}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text>

View File

@@ -42,7 +42,7 @@ export default function CreateBerita() {
<Stack gap={"xs"}>
<Title order={3}>Create Daftar Informasi Publik Desa Darmasaba</Title>
<TextInput
label="Jenis Informasi"
label={<Text fz={"sm"} fw={"bold"}>Jenis Informasi</Text>}
placeholder="masukkan jenis informasi"
onChange={(val) => {
daftarInformasi.create.form.jenisInformasi = val.target.value
@@ -58,13 +58,13 @@ export default function CreateBerita() {
/>
</Box>
<TextInput
label="Tanggal Publikasi"
placeholder="masukkan tanggal publikasi"
onChange={(val) => {
daftarInformasi.create.form.tanggal = val.target.value
}}
label={<Text fz={"sm"} fw={"bold"}>Tanggal Publikasi</Text>}
type="date"
placeholder="Contoh: 2022-01-01"
value={daftarInformasi.create.form.tanggal}
onChange={(e) => (daftarInformasi.create.form.tanggal = e.currentTarget.value)}
/>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan Berita</Button>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>

View File

@@ -1,12 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCircleDashedPlus, IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
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 { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import daftarInformasiPublik from '../../_state/ppid/daftar_informasi_publik/daftarInformasiPublik';
function DaftarInformasiPublik() {
@@ -14,7 +15,7 @@ function DaftarInformasiPublik() {
return (
<Box>
<HeaderSearch
title='Posisi Organisasi'
title='Daftar Informasi Publik Desa Darmasaba'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
@@ -29,12 +30,14 @@ function ListDaftarInformasi({ search }: { search: string }) {
const listData = useProxy(daftarInformasiPublik)
const router = useRouter()
useShallowEffect(() => {
listData.findMany.load()
}, [])
const { data, page, totalPages, loading, load } = listData.findMany
useEffect(() => {
load(page, 10)
}, [page])
const filteredData = (listData.findMany.data || []).filter(item => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.jenisInformasi.toLowerCase().includes(keyword) ||
@@ -42,28 +45,47 @@ function ListDaftarInformasi({ search }: { search: string }) {
);
});
if (!listData.findMany.data) {
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={790} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data daftar informasi publik yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 870, md: 790 }}>
<Stack>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Text fz={"xl"} fw={"bold"}>List Daftar Informasi Publik Desa Darmasaba</Text>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button onClick={() => router.push("/admin/ppid/daftar-informasi-publik-desa-darmasaba/create")} bg={colors['blue-button']}>
<IconCircleDashedPlus size={25} />
</Button>
</GridCol>
</Grid>
<JudulList
title='List Daftar Informasi Publik Desa Darmasaba'
href='/admin/ppid/daftar-informasi-publik-desa-darmasaba/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table
striped
@@ -74,19 +96,19 @@ function ListDaftarInformasi({ search }: { search: string }) {
>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Jenis Informasi</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Detail</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Jenis Informasi</TableTh>
<TableTh style={{ width: '50%' }}>Deskripsi</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>{item.jenisInformasi}</TableTd>
<TableTd dangerouslySetInnerHTML={{ __html: item.deskripsi }}></TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ wordWrap: 'break-word' }}>{item.jenisInformasi}</TableTd>
<TableTd style={{ wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
@@ -98,6 +120,18 @@ function ListDaftarInformasi({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
)
}

View File

@@ -24,16 +24,20 @@ function GrafikBerdasarkanJenisKelaminRespondenCreate() {
}
const handleSubmit = async () => {
const id = await stategrafikBerdasarkanJenisKelamin.create.create();
if (id) {
const idStr = String(id);
await stategrafikBerdasarkanJenisKelamin.findUnique.load(idStr);
if (stategrafikBerdasarkanJenisKelamin.findUnique.data) {
setDonutData([stategrafikBerdasarkanJenisKelamin.findUnique.data]);
try {
const id = await stategrafikBerdasarkanJenisKelamin.create.create();
if (typeof id !== 'undefined') {
const idStr = String(id);
await stategrafikBerdasarkanJenisKelamin.findUnique.load(idStr);
if (stategrafikBerdasarkanJenisKelamin.findUnique.data) {
setDonutData([stategrafikBerdasarkanJenisKelamin.findUnique.data]);
}
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden");
} catch (error) {
console.error('Error submitting form:', error);
}
resetForm();
router.push("/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden")
}
return (
<Box>

View File

@@ -2,16 +2,16 @@
'use client'
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function GrafikBerdasarkanJenisKelamin() {
const [search, setSearch] = useState("");
@@ -36,6 +36,32 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanJenisKelamin.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
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', value: totalLaki, color: colors['blue-button'], key: 'laki' },
{ name: 'perempuan', value: totalPerempuan, color: '#10A85AFF', key: 'perempuan' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.laki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
@@ -47,55 +73,56 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true);
stategrafikBerdasarkanJenisKelamin.findMany.load()
}, []);
useEffect(() => {
if (stategrafikBerdasarkanJenisKelamin.findMany.data) {
const totalLaki = stategrafikBerdasarkanJenisKelamin.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
const totalPerempuan = stategrafikBerdasarkanJenisKelamin.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
setDonutData([
{ name: 'laki', value: totalLaki, color: colors['blue-button'], key: 'laki' },
{ name: 'perempuan', value: totalPerempuan, color: '#10A85AFF', key: 'perempuan' }
]);
}
}, [stategrafikBerdasarkanJenisKelamin.findMany.data])
const filteredData = (stategrafikBerdasarkanJenisKelamin.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.laki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
});
if (!stategrafikBerdasarkanJenisKelamin.findMany.data) {
}
if (data.length === 0) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Box py={10}>
<Paper p="md">
<Stack>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Laki-laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data berdasarkan jenis kelamin responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
title='List Grafik Berdasarkan Jenis Kelamin Responden'
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Jenis Kelamin Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Laki-laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Laki-laki</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Perempuan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -106,16 +133,17 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.laki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.laki}</TableTd>
<TableTd style={{ width: '20%', textAlign: 'center' }}>{item.perempuan}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_jenis_kelamin_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanJenisKelamin.delete.loading}
@@ -130,16 +158,27 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
))
)}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
{mounted && donutData.length === 0 ? (<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>) : (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}
data={donutData}
@@ -168,8 +207,6 @@ function ListGrafikBerdasarkanJenisKelamin({ search }: { search: string }) {
<Text>Perempuan: {donutData.find((entry) => entry.name === 'perempuan')?.value}</Text>
</Flex>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>

View File

@@ -45,7 +45,7 @@ function GrafikBerdasarkanRespondenCreate() {
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
<Title order={3}>Grafik Hasil Kepuasan Masyarakat Berdasarkan Responden</Title>
<TextInput
label="Sangat Baik"
type='number'

View File

@@ -1,17 +1,17 @@
'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useSnapshot } from 'valtio';
import JudulListTab from '../../../_com/jusulListTab';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikBerdasarkanResponden from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import HeaderSearch from '../../../_com/header';
function GrafikBerdasarkanResponden() {
const [search, setSearch] = useState("");
@@ -37,24 +37,14 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const handleDelete = async () => {
if (selectedId) {
await stategrafikBerdasarkanResponden.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
// Refresh data agar chart & tabel ikut update
stategrafikBerdasarkanResponden.findMany.load();
}
}
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanResponden.findMany
useShallowEffect(() => {
setMounted(true)
stategrafikBerdasarkanResponden.findMany.load()
}, [])
load(page, 10)
}, [page])
const filteredData = (stategrafikBerdasarkanResponden.findMany.data || []).filter(item => {
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.sangatbaik.toString().toLowerCase().includes(keyword) ||
@@ -65,11 +55,11 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
});
useEffect(() => {
if (stategrafikBerdasarkanResponden.findMany.data) {
const totalSangatBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
const totalBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
const totalKurangBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
const totalTidakBaik = stategrafikBerdasarkanResponden.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
if (data) {
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' },
@@ -78,34 +68,72 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
]);
}
}, [stategrafikBerdasarkanResponden.findMany.data])
}, [data])
if (!stategrafikBerdasarkanResponden.findMany.data) {
const handleDelete = async () => {
if (selectedId) {
await stategrafikBerdasarkanResponden.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
// Refresh data agar chart & tabel ikut update
stategrafikBerdasarkanResponden.findMany.load();
}
}
if (loading || !data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Sangat Baik</TableTh>
<TableTh>Baik</TableTh>
<TableTh>Kurang Baik</TableTh>
<TableTh>Tidak Baik</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<JudulListTab
title='List Grafik Berdasarkan Pilihan Responden'
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Berdasarkan Pilihan Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Sangat Baik</TableTh>
<TableTh>Baik</TableTh>
<TableTh>Kurang Baik</TableTh>
<TableTh>Tidak Baik</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Sangat Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Kurang Baik</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Tidak Baik</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -116,18 +144,19 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
</TableTd>
</TableTr>
) : (
filteredData.map((item) => (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.sangatbaik}</TableTd>
<TableTd>{item.baik}</TableTd>
<TableTd>{item.kurangbaik}</TableTd>
<TableTd>{item.tidakbaik}</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{item.sangatbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.baik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.kurangbaik}</TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}>{item.tidakbaik}</TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_responden/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ width: '5%', textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanResponden.delete.loading}
@@ -145,12 +174,24 @@ function ListGrafikBerdasarkanResponden({ search }: { search: string }) {
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Title pb={10} order={3}>Grafik Berdasarkan Pilihan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}

View File

@@ -2,16 +2,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function GrafikBerdasarakanUmur() {
const [search, setSearch] = useState("");
@@ -36,6 +37,37 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
const { data, page, totalPages, loading, load } = stategrafikBerdasarkanUmur.findMany
useShallowEffect(() => {
setMounted(true);
load(page, 10)
}, [page]);
useEffect(() => {
if (data) {
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' }
]);
}
}, [data])
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.remaja.toString().toLowerCase().includes(keyword) ||
item.dewasa.toString().toLowerCase().includes(keyword) ||
item.orangtua.toString().toLowerCase().includes(keyword) ||
item.lansia.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = async () => {
if (selectedId) {
@@ -47,50 +79,48 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true);
stategrafikBerdasarkanUmur.findMany.load()
}, []);
useEffect(() => {
if (stategrafikBerdasarkanUmur.findMany.data) {
const totalRemaja = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
const totalDewasa = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
const totalOrangtua = stategrafikBerdasarkanUmur.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
const totalLansia = stategrafikBerdasarkanUmur.findMany.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' }
]);
}
}, [stategrafikBerdasarkanUmur.findMany.data])
const filteredData = (stategrafikBerdasarkanUmur.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.remaja.toString().toLowerCase().includes(keyword) ||
item.dewasa.toString().toLowerCase().includes(keyword) ||
item.orangtua.toString().toLowerCase().includes(keyword) ||
item.lansia.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={730} />
</Stack>
);
});
if (!stategrafikBerdasarkanUmur.findMany.data) {
}
if (data.length === 0) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Remaja</TableTh>
<TableTh>Dewasa</TableTh>
<TableTh>Orangtua</TableTh>
<TableTh>Lansia</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik berdasarkan umur responden yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={"md"}>
<Paper bg={colors['white-1']} p={"md"} h={{ base: 730, md: 650 }}>
<JudulListTab
title='List Grafik Berdasarkan Umur Responden'
title='List Data Berdasarkan Umur Responden'
href='/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
@@ -98,12 +128,13 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Remaja</TableTh>
<TableTh>Dewasa</TableTh>
<TableTh>Orangtua</TableTh>
<TableTh>Lansia</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '2%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Remaja</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Dewasa</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Orangtua</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Lansia</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
@@ -116,16 +147,17 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
) : (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.remaja}</TableTd>
<TableTd>{item.dewasa}</TableTd>
<TableTd>{item.orangtua}</TableTd>
<TableTd>{item.lansia}</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{filteredData.indexOf(item) + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.remaja}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.dewasa}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.orangtua}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.lansia}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_berdasarkan_umur/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stategrafikBerdasarkanUmur.delete.loading}
@@ -143,12 +175,24 @@ function ListGrafikBerdasarakanUmur({ search }: { search: string }) {
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Title pb={10} order={3}>Grafik Umur Berdasarkan Responden</Title>
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
width={800} height={300}

View File

@@ -1,16 +1,16 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
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 { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useSnapshot } from 'valtio';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikHasilKepuasanMasyarakat from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
import HeaderSearch from '../../../_com/header';
function GrafikHasilKepuasanMasyarakat() {
@@ -46,8 +46,34 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter();
const { data, page, totalPages, loading, load } = stateGrafikHasilKepuasan.findMany
useShallowEffect(() => {
setMounted(true)
load(page, 10)
}, [page])
useEffect(() => {
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [data]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.label.toLowerCase().includes(keyword) ||
item.kepuasan.toString().toLowerCase().includes(keyword)
);
});
const handleDelete = () => {
if (selectedId) {
stateGrafikHasilKepuasan.delete.byId(selectedId)
@@ -58,70 +84,69 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
}
}
useShallowEffect(() => {
setMounted(true)
stateGrafikHasilKepuasan.findMany.load()
}, [])
useEffect(() => {
if (stateGrafikHasilKepuasan.findMany.data) {
setChartData(
stateGrafikHasilKepuasan.findMany.data.map((item) => ({
id: item.id,
label: item.label,
kepuasan: Number(item.kepuasan),
}))
);
}
}, [stateGrafikHasilKepuasan.findMany.data]);
const filteredData = (stateGrafikHasilKepuasan.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
if (loading || !data) {
return (
item.label.toLowerCase().includes(keyword) ||
item.kepuasan.toString().toLowerCase().includes(keyword)
);
});
if (!stateGrafikHasilKepuasan.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
<Stack py={10}>
<Skeleton height={730} />
</Stack>
)
);
}
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Label</TableTh>
<TableTh>Jumlah Kepuasan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data grafik hasil kepuasan masyarakat yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box>
<Stack gap={"xs"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Grafik Hasil Kepuasan Masyarakat'
<Paper bg={colors['white-1']} p={'md'} h={{ base: 730, md: 650 }}>
<JudulList
title='List Data Hasil Kepuasan Masyarakat'
href='/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/create'
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Label</TableTh>
<TableTh>Jumlah Kepuasan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Label</TableTh>
<TableTh style={{ width: '20%', textAlign: 'center' }}>Jumlah Kepuasan</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Edit</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{item.label}</TableTd>
<TableTd>{item.kepuasan}</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.label}</TableTd>
<TableTd style={{ textAlign: 'center' }}>{item.kepuasan}</TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button color='green' onClick={() => router.push(`/admin/ppid/ikm-desa-darmasaba/grafik_hasil_kepuasan_masyarakat/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<TableTd style={{ textAlign: 'center' }}>
<Button
color='red'
disabled={stateGrafikHasilKepuasan.delete.loading}
@@ -137,12 +162,24 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo(0, 0);
}}
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
<Box style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }}>
<Paper style={{ width: '100%', minWidth: 300, height: 500, minHeight: 300 }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title pb={10} order={3}>Data Kepuasan Masyarakat</Title>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 300 : isTablet ? 300 : 300} height={380} data={chartData} >
<XAxis dataKey="label" />

View File

@@ -230,12 +230,12 @@ export const navBar = [
{
id: "Ekonomi_4",
name: "PADesa (Pendapatan Asli Desa)",
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa"
path: "/admin/ekonomi/PADesa-pendapatan-asli-desa/apbdesa"
},
{
id: "Ekonomi_5",
name: "Jumlah Pengangguran 2024-2025",
path: "/admin/ekonomi/jumlah-pengangguran-2024-2025"
name: "Jumlah Pengangguran",
path: "/admin/ekonomi/jumlah-pengangguran"
},
{
id: "Ekonomi_6",
@@ -244,8 +244,8 @@ export const navBar = [
},
{
id: "Ekonomi_7",
name: "Jumlah Penduduk Miskin 2024-2025",
path: "/admin/ekonomi/jumlah-penduduk-miskin-2024-2025"
name: "Jumlah Penduduk Miskin",
path: "/admin/ekonomi/jumlah-penduduk-miskin"
},
{
id: "Ekonomi_8",

View File

@@ -1,25 +1,42 @@
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function beritaFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const skip = (page - 1) * limit;
async function beritaFindMany() {
try {
const data = await prisma.berita.findMany({
where: { isActive: true },
include: {
image: true,
kategoriBerita: true,
},
});
const [data, total] = await Promise.all([
prisma.berita.findMany({
where: { isActive: true },
include: {
image: true,
kategoriBerita: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
}),
prisma.berita.count({
where: { isActive: true }
})
]);
return {
success: true,
message: "Success fetch berita",
message: "Success fetch berita with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error("Find many error:", e);
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch berita",
message: "Failed fetch berita with pagination",
};
}
}

View File

@@ -9,6 +9,8 @@ import GrafikMenganggurBerdasarkanPendidikan from "./usia-kerja-yang-menganggur/
import JumlahPendudukMiskin from "./jumlah-penduduk-miskin";
import SektorUnggulanDesa from "./sektor-unggulan-desa";
import DemografiPekerjaan from "./demografi-pekerjaan";
import JumlahPengangguran from "./jumlah-pengangguran";
import PendapatanAsliDesa from "./pendapatan-asli-desa";
const Ekonomi = new Elysia({
prefix: "/api/ekonomi",
@@ -24,5 +26,7 @@ const Ekonomi = new Elysia({
.use(JumlahPendudukMiskin)
.use(SektorUnggulanDesa)
.use(DemografiPekerjaan)
.use(JumlahPengangguran)
.use(PendapatanAsliDesa)
export default Ekonomi

View File

@@ -0,0 +1,139 @@
// import prisma from "@/lib/prisma";
// import { Prisma } from "@prisma/client";
// import { Context } from "elysia";
// type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
// select: {
// month: true;
// year: true;
// totalUnemployment: true;
// educatedUnemployment: true;
// uneducatedUnemployment: true;
// percentageChange: true;
// };
// }>;
// export default async function detailDataPengangguranCreate(context: Context) {
// const body = context.body as FormCreate;
// // Cek apakah data untuk bulan & tahun tersebut sudah ada
// const existing = await prisma.detailDataPengangguran.findFirst({
// where: {
// month: body.month,
// year: body.year,
// },
// });
// if (existing) {
// return {
// success: false,
// message: `Data bulan ${body.month} ${body.year} sudah ada.`,
// data: null,
// };
// }
// const created = await prisma.detailDataPengangguran.create({
// data: {
// month: body.month,
// year: body.year,
// totalUnemployment: body.totalUnemployment,
// educatedUnemployment: body.educatedUnemployment,
// uneducatedUnemployment: body.uneducatedUnemployment,
// percentageChange: body.percentageChange ?? null,
// },
// select: {
// id: true,
// month: true,
// year: true,
// totalUnemployment: true,
// educatedUnemployment: true,
// uneducatedUnemployment: true,
// percentageChange: true,
// },
// });
// return {
// success: true,
// message: "Berhasil menambahkan data pengangguran bulanan",
// data: created,
// };
// }
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
select: {
month: true;
year: true;
totalUnemployment: true;
educatedUnemployment: true;
uneducatedUnemployment: true;
};
}>;
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
export default async function detailDataPengangguranCreate(context: Context) {
const body = context.body as FormCreate;
const existing = await prisma.detailDataPengangguran.findFirst({
where: {
month: body.month,
year: body.year,
},
});
if (existing) {
return {
success: false,
message: `Data bulan ${body.month} ${body.year} sudah ada.`,
data: null,
};
}
// Cari bulan sebelumnya
const currentMonthIndex = monthOrder.indexOf(body.month);
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
const prevYear = currentMonthIndex > 0 ? body.year : body.year - 1;
const prevData = await prisma.detailDataPengangguran.findFirst({
where: {
month: prevMonth,
year: prevYear,
},
});
let percentageChange: number | null = null;
if (prevData) {
const change = ((body.totalUnemployment - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
const created = await prisma.detailDataPengangguran.create({
data: {
month: body.month,
year: body.year,
totalUnemployment: body.totalUnemployment,
educatedUnemployment: body.educatedUnemployment,
uneducatedUnemployment: body.uneducatedUnemployment,
percentageChange,
},
select: {
id: true,
month: true,
year: true,
totalUnemployment: true,
educatedUnemployment: true,
uneducatedUnemployment: true,
percentageChange: true,
},
});
return {
success: true,
message: "Berhasil menambahkan data pengangguran bulanan",
data: created,
};
}

Some files were not shown because too many files have changed in this diff Show More