Compare commits

...

13 Commits

Author SHA1 Message Date
9dfcda7687 Pertama 2025-07-21 15:23:50 +08:00
e2f75ff3ad API & UI Menu Lingkungan, Submenu Edukas Lingkungan 2025-07-18 21:24:51 +08:00
f05a096633 pertama 2025-07-18 17:42:10 +08:00
9c55869aa6 pertama 2025-07-18 17:40:57 +08:00
6e5d45fa20 pertama 2025-07-18 17:31:31 +08:00
e5373b4823 pertama 2025-07-18 17:28:55 +08:00
928cd048c0 pertama 2025-07-18 17:27:54 +08:00
41f54772e9 API & UI Menu Lingkungan, Submenu Data Lingkungan Desa 2025-07-18 17:11:13 +08:00
cd343badb2 API & UI Menu Lingkungan, Submenu Program Penghijauan 2025-07-18 16:27:38 +08:00
4025771a4d API & UI Menu Lingkungan Submenu Pengelolaan Sampah 2025-07-18 15:01:43 +08:00
7439eb7687 API & UI Menu Lingkungan, Submenu Penglolaan Sampah 2025-07-17 17:09:28 +08:00
49a1084099 API & UI Menu Inovasi, SubMenu Layanan Online Desa 2025-07-17 15:31:10 +08:00
cde6c91cd4 API & UI Menu Inovasi, Submenu Layanan Online Desa: Tab 1-4 2025-07-17 11:44:53 +08:00
159 changed files with 8131 additions and 1470 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -40,6 +40,7 @@
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"@types/bun": "^1.2.2",
"@types/leaflet": "^1.9.20",
"@types/lodash": "^4.17.16",
"add": "^2.0.6",
"animate.css": "^4.1.1",
@@ -53,6 +54,8 @@
"framer-motion": "^12.23.5",
"get-port": "^7.1.0",
"jotai": "^2.12.3",
"leaflet": "^1.9.4",
"list": "^2.0.19",
"lodash": "^4.17.21",
"motion": "^12.4.1",
"nanoid": "^5.1.5",
@@ -63,6 +66,7 @@
"prisma": "^6.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5",
"readdirp": "^4.1.1",

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Contoh Kegiatan di Desa Darmasaba",
"deskripsi": "<ul><li><p>Pelatihan membuat kompos dari sampah rumah tangga</p></li><li><p>Gerakan Jumat Bersih rutin</p></li><li><p>Workshop membuat ecobrick</p></li><li><p>Lomba kebersihan antar banjar</p></li><li><p>Sosialisasi lingkungan di sekolah dan posyandu</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Materi Edukasi yang Diberikan",
"deskripsi": "<ul><li><p>Pengelolaan Sampah (Pilah sampah organik dan anorganik)</p></li><li><p>Pencegahan pencemaran lingkungan (air, udara, dan tanah)</p></li><li><p>Pemanfaatan lahan hijau dan penghijauan desa</p></li><li><p>Daur ulang dan kreatifitas dari sampah</p></li><li><p>Bahaya pembakaran sampah sembarangan</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Tujuan Edukasi Lingkungan",
"deskripsi": "<ul><li><p>Meningkatkan kesadaran masyarakat tentang pentingnya lingkungan bersih dan sehat</p></li><li><p>Mendorong partisipasi warga dalam kegiatan pengelolaan sampah, penghijauan, dan konservasi</p></li><li><p>Mengurangi dampak negatif terhadap lingkungan dari kegiatan manusia</p></li><li><p>Membentuk generasi muda yang peduli terhadap isu-isu lingkungan</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Bentuk Konservasi Berdasarkan Adat",
"deskripsi": "<ul><li><p>Pelestarian Hutan Adat seperti Alas Pala Sangeh atau Wana Kerthi</p></li><li><p>Subak: Sistem pengelolaan irigasi tradisional yang menjunjung kebersamaan dan keberlanjutan</p></li><li><p>Hari Raya Tumpek Uduh: Perayaan khusus untuk menghormati pohon dan tumbuhan</p></li><li><p>Perarem dan Awig-Awig: Aturan adat desa yang mengatur larangan menebang pohon sembarangan, membuang limbah ke sungai, dll.</p></li><li><p>Ritual penyucian alam seperti Melasti, Piodalan Segara, dan lainnya</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Filosofi Tri Hita Karuna",
"deskripsi": "<ul><li><p>Parahyangan: Hubungan manusia dengan Tuhan</p></li><li><p>Pawongan: Hubungan antar manusia</p></li><li><p>Palemahan: Hubungan manusia dengan alam</p></li></ul>"
}
]

View File

@@ -0,0 +1,7 @@
[
{
"id": "edit",
"judul": "Nilai Konservasi Adat",
"deskripsi": "<ul><li><p>Menjaga keseimbangan ekosistem</p></li><li><p>Melestarikan spiritualitas lokal dan kesucian alam</p></li><li><p>Menumbuhkan kesadaran kolektif untuk hidup selaras dengan lingkungan</p></li><li><p>Menjaga keberlangsungan sumber daya alam untuk generasi mendatang</p></li></ul>"
}
]

View File

@@ -0,0 +1,67 @@
-- CreateTable
CREATE TABLE "PengaduanMasyarakat" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"nomorTelepon" TEXT NOT NULL,
"nik" TEXT NOT NULL,
"judulPengaduan" TEXT NOT NULL,
"lokasiKejadian" TEXT NOT NULL,
"imageId" TEXT NOT NULL,
"deskripsiPengaduan" TEXT NOT NULL,
"jenisPengaduanId" TEXT NOT NULL,
"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 "PengaduanMasyarakat_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "JenisPengaduan" (
"id" TEXT NOT NULL,
"nama" TEXT NOT NULL,
"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 "JenisPengaduan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PengelolaanSampah" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"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 "PengelolaanSampah_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "KeteranganBankSampahTerdekat" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"alamat" TEXT NOT NULL,
"namaTempatMaps" TEXT NOT NULL,
"linkPetunjukArah" TEXT NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
"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 "KeteranganBankSampahTerdekat_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_jenisPengaduanId_fkey" FOREIGN KEY ("jenisPengaduanId") REFERENCES "JenisPengaduan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -74,28 +74,18 @@ model FileStorage {
ProgramKesehatan ProgramKesehatan[]
PenangananDarurat PenangananDarurat[]
KontakDarurat KontakDarurat[]
InfoWabahPenyakit InfoWabahPenyakit[]
KeamananLingkungan KeamananLingkungan[]
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
InfoWabahPenyakit InfoWabahPenyakit[]
KeamananLingkungan KeamananLingkungan[]
MenuTipsKeamanan MenuTipsKeamanan[]
Pelapor Pelapor[]
PasarDesa PasarDesa[]
KontakDaruratKeamanan KontakDaruratKeamanan[]
KontakItem KontakItem[]
Pegawai Pegawai[]
DesaDigital DesaDigital[]
KolaborasiInovasi KolaborasiInovasi[]
InfoTekno InfoTekno[]
PengaduanMasyarakat PengaduanMasyarakat[]
}
//========================================= MENU PPID ========================================= //
@@ -1405,25 +1395,168 @@ model AjukanIdeInovatif {
// ========================================= LAYANAN ONLINE DESA ========================================= //
model AdministrasiOnline {
id String @id @default(cuid())
name String
alamat String
nomorTelepon String
jenisLayanan JenisLayanan @relation(fields: [jenisLayananId], references: [id])
jenisLayananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
id String @id @default(cuid())
name String
alamat String
nomorTelepon String
jenisLayanan JenisLayanan @relation(fields: [jenisLayananId], references: [id])
jenisLayananId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisLayanan {
id String @id @default(uuid())
nama String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AdministrasiOnline AdministrasiOnline[]
id String @id @default(uuid())
nama String
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
AdministrasiOnline AdministrasiOnline[]
}
model PengaduanMasyarakat {
id String @id @default(cuid())
name String
email String
nomorTelepon String
nik String
judulPengaduan String
lokasiKejadian String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
deskripsiPengaduan String @db.Text
jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id])
jenisPengaduanId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model JenisPengaduan {
id String @id @default(uuid())
nama String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
PengaduanMasyarakat PengaduanMasyarakat[]
}
// ========================================= LINGKUNGAN ========================================= //
// ========================================= PENGELOLAAN SAMPAH ========================================= //
model PengelolaanSampah {
id String @id @default(cuid())
name String
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model KeteranganBankSampahTerdekat {
id String @id @default(cuid())
name String
alamat String
namaTempatMaps String
linkPetunjukArah String
lat Float
lng Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PORGRAM PENGHIJAUAN ========================================= //
model ProgramPenghijauan {
id String @id @default(cuid())
name String
judul 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)
}
// ========================================= DATA LINGKUNGAN DESA ========================================= //
model DataLingkunganDesa {
id String @id @default(cuid())
name String
jumlah String
deskripsi String @db.Text //deskripsi panjang
icon String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= EDUKASI LINGKUNGAN ========================================= //
model TujuanEdukasiLingkungan {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model MateriEdukasiLingkungan {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model ContohEdukasiLingkungan {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KONSERVASI ADAT BALI ========================================= //
model FilosofiTriHita {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model BentukKonservasiBerdasarkanAdat {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
model NilaiKonservasiAdat {
id String @id @default(cuid())
judul String @db.Text
deskripsi String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -22,6 +22,13 @@ import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-orga
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';
import tujuanEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json';
import materiEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json';
import contohEdukasiLingkungan from './data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json';
import nilaiKonservasiAdat from './data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json';
import bentukKonservasiBerdasarkanAdat from './data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json';
import filosofiTriHita from './data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json';
(async () => {
for (const l of layanan) {
@@ -455,6 +462,122 @@ import detailDataPengangguran from './data/ekonomi/jumlah-pengangguran/detail-da
});
}
console.log("📊 detailDataPengangguran success ...");
for (const e of tujuanEdukasiLingkungan) {
await prisma.tujuanEdukasiLingkungan.upsert({
where: {
id: e.id,
},
update: {
judul: e.judul,
deskripsi: e.deskripsi,
},
create: {
id: e.id,
judul: e.judul,
deskripsi: e.deskripsi,
},
});
}
console.log("tujuan edukasi lingkungan success ...");
for (const m of materiEdukasiLingkungan) {
await prisma.materiEdukasiLingkungan.upsert({
where: {
id: m.id,
},
update: {
judul: m.judul,
deskripsi: m.deskripsi,
},
create: {
id: m.id,
judul: m.judul,
deskripsi: m.deskripsi,
},
});
}
console.log("materi edukasi lingkungan success ...");
for (const c of contohEdukasiLingkungan) {
await prisma.contohEdukasiLingkungan.upsert({
where: {
id: c.id,
},
update: {
judul: c.judul,
deskripsi: c.deskripsi,
},
create: {
id: c.id,
judul: c.judul,
deskripsi: c.deskripsi,
},
});
}
console.log("contoh edukasi lingkungan success ...");
for (const f of filosofiTriHita) {
await prisma.filosofiTriHita.upsert({
where: {
id: f.id,
},
update: {
judul: f.judul,
deskripsi: f.deskripsi,
},
create: {
id: f.id,
judul: f.judul,
deskripsi: f.deskripsi,
},
});
}
console.log("filosofi tri hita success ...");
for (const b of bentukKonservasiBerdasarkanAdat) {
await prisma.bentukKonservasiBerdasarkanAdat.upsert({
where: {
id: b.id,
},
update: {
judul: b.judul,
deskripsi: b.deskripsi,
},
create: {
id: b.id,
judul: b.judul,
deskripsi: b.deskripsi,
},
});
}
console.log("bentuk konservasi berdasarkan adat success ...");
for (const n of nilaiKonservasiAdat) {
await prisma.nilaiKonservasiAdat.upsert({
where: {
id: n.id,
},
update: {
judul: n.judul,
deskripsi: n.deskripsi,
},
create: {
id: n.id,
judul: n.judul,
deskripsi: n.deskripsi,
},
});
}
console.log("nilai konservasi adat success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
import { useEffect, useState } from 'react';
import 'leaflet/dist/leaflet.css';
import L, { LeafletMouseEvent } from 'leaflet';
type Props = {
defaultCenter: { lat: number; lng: number };
onSelect?: (pos: { lat: number; lng: number }) => void;
readOnly?: boolean;
};
export default function LeafletMap({ defaultCenter, onSelect, readOnly = false }: Props) {
const [markerPos, setMarkerPos] = useState(defaultCenter);
useEffect(() => {
// Aman di sini, karena ini hanya jalan di client
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
}, []);
function LocationMarker() {
useMapEvents({
click(e: LeafletMouseEvent) {
if (readOnly) return;
const { lat, lng } = e.latlng;
setMarkerPos({ lat, lng });
onSelect?.({ lat, lng });
},
});
return <Marker position={markerPos} />;
}
return (
<MapContainer
center={defaultCenter}
zoom={16}
scrollWheelZoom
style={{ height: '100%', width: '100%', zIndex: 0 }}
>
<TileLayer
attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a>'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LocationMarker />
</MapContainer>
);
}

View File

@@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { MapContainer, TileLayer, Marker, useMapEvents } from 'react-leaflet';
import { useState, useEffect } from 'react';
import 'leaflet/dist/leaflet.css';
import L, { LeafletMouseEvent } from 'leaflet';
type Props = {
initialPosition: { lat: number; lng: number };
onChange: (pos: { lat: number; lng: number }) => void;
};
export default function LeafletMapEdit({ initialPosition, onChange }: Props) {
const [markerPos, setMarkerPos] = useState(initialPosition);
// ✅ Pastikan icon config cuma jalan di client
useEffect(() => {
if (typeof window !== 'undefined') {
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl:
'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
}
}, []);
useEffect(() => {
setMarkerPos(initialPosition);
}, [initialPosition]);
function LocationMarker() {
useMapEvents({
click(e: LeafletMouseEvent) {
const { lat, lng } = e.latlng;
setMarkerPos({ lat, lng });
onChange({ lat, lng });
},
});
return <Marker position={markerPos} />;
}
return (
<MapContainer
center={markerPos}
zoom={16}
scrollWheelZoom
style={{ height: '100%', width: '100%', zIndex: 0 }}
>
<TileLayer
attribution='&copy; <a href="https://osm.org/copyright">OpenStreetMap</a>'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LocationMarker />
</MapContainer>
);
}

View File

@@ -4,10 +4,21 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconHome,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconShieldFilled,
IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled,
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
@@ -17,6 +28,18 @@ const iconMap = {
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
truck: { label: 'Truck', icon: IconTruckFilled },
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet}
};
type IconKey = keyof typeof iconMap;

View File

@@ -3,10 +3,21 @@
import { Box, rem, Select } from '@mantine/core';
import {
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconHome,
IconHomeEco,
IconLeaf,
IconRecycle,
IconScale,
IconShieldFilled,
IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled,
} from '@tabler/icons-react';
const iconMap = {
@@ -15,6 +26,17 @@ const iconMap = {
wisata: { label: 'Wisata', icon: IconTent },
ekonomi: { label: 'Ekonomi', icon: IconChartLine },
sampah: { label: 'Sampah', icon: IconRecycle },
truck: { label: 'Truck', icon: IconTruckFilled },
scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco},
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled},
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp},
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet}
};
type IconKey = keyof typeof iconMap;

View File

@@ -263,6 +263,59 @@ const berita = proxy({
berita.edit.form = { ...defaultForm };
},
},
findFirst: {
data: null as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null,
loading: false,
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.desa.berita["find-first"].get();
if (res.status === 200 && res.data?.success) {
// Add type assertion to ensure type safety
berita.findFirst.data = res.data.data as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}> | null;
}
} catch (err) {
console.error("Gagal fetch berita terbaru:", err);
} finally {
this.loading = false;
}
},
},
findRecent: {
data: [] as Prisma.BeritaGetPayload<{
include: {
image: true;
kategoriBerita: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.berita["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch berita recent:", error);
} finally {
this.loading = false;
}
},
}
});

View File

@@ -68,11 +68,11 @@ const pengumuman = proxy({
},
findMany: {
data: null as
| Prisma.PengumumanGetPayload<{
include: {
| Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
}
}>[]
};
}>[]
| null,
async load() {
const res = await ApiFetch.api.desa.pengumuman["find-many"].get();
@@ -82,30 +82,28 @@ const pengumuman = proxy({
}
},
},
// findUnique: {
// data: null as
// | Prisma.PengumumanGetPayload<{
// include: {
// CategoryPengumuman: true;
// }
// }>
// | null,
// async load(id: string) {
// try {
// const res = await fetch(`/api/desa/pengumuman/${id}`);
// if (res.ok) {
// const data = await res.json();
// pengumuman.findUnique.data = data.data ?? null;
// } else {
// console.error('Failed to fetch pengumuman:', res.statusText);
// pengumuman.findUnique.data = null;
// }
// } catch (error) {
// console.error('Error fetching pengumuman:', error);
// pengumuman.findUnique.data = null;
// }
// },
// },
findUnique: {
data: null as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/desa/pengumuman/${id}`);
if (res.ok) {
const data = await res.json();
pengumuman.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengumuman:", res.statusText);
pengumuman.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching pengumuman:", error);
pengumuman.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
@@ -237,6 +235,55 @@ const pengumuman = proxy({
}
},
},
findFirst: {
data: null as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null,
loading: false,
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.desa.pengumuman["find-first"].get();
if (res.status === 200 && res.data?.success) {
// Add type assertion to ensure type safety
pengumuman.findFirst.data = res.data
.data as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}> | null;
}
} catch (err) {
console.error("Gagal fetch pengumuman terbaru:", err);
} finally {
this.loading = false;
}
},
},
findRecent: {
data: [] as Prisma.PengumumanGetPayload<{
include: {
CategoryPengumuman: true;
};
}>[],
loading: false,
async load() {
try {
this.loading = true;
const res = await ApiFetch.api.desa.pengumuman["find-recent"].get();
if (res.status === 200 && res.data?.success) {
this.data = res.data.data ?? [];
}
} catch (error) {
console.error("Gagal fetch pengumuman recent:", error);
} finally {
this.loading = false;
}
},
},
});
const stateDesaPengumuman = proxy({

View File

@@ -395,9 +395,409 @@ const jenisLayanan = proxy({
},
});
// ========================================= PENGADUAN MASYARAKAT ========================================= //
const templatePengaduanMasyarakatForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
email: z.string().min(1, "Alamat minimal 1 karakter"),
nomorTelepon: z.string().min(1, "Nomor telepon minimal 1 karakter"),
nik: z.string().min(1, "NIK minimal 1 karakter"),
judulPengaduan: z.string().min(1, "Judul pengaduan minimal 1 karakter"),
lokasiKejadian: z.string().min(1, "Lokasi kejadian minimal 1 karakter"),
deskripsiPengaduan: z.string().min(1, "Deskripsi pengaduan minimal 1 karakter"),
jenisPengaduanId: z.string().min(1, "Jenis pengaduan minimal 1 karakter"),
imageId: z.string().min(1, "Image minimal 1 karakter"),
});
const defaultPengaduanMasyarakatForm = {
name: "",
email: "",
nomorTelepon: "",
nik: "",
judulPengaduan: "",
lokasiKejadian: "",
deskripsiPengaduan: "",
jenisPengaduanId: "",
imageId: "",
};
const pengaduanMasyarakat = proxy({
create: {
form: { ...defaultPengaduanMasyarakatForm },
loading: false,
async create() {
const cek = templatePengaduanMasyarakatForm.safeParse(
pengaduanMasyarakat.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengaduanMasyarakat.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"create"
].post(pengaduanMasyarakat.create.form);
if (res.status === 200) {
pengaduanMasyarakat.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
pengaduanMasyarakat.create.loading = false;
}
},
},
findMany: {
data: null as Array<
Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}>
> | null,
page: 1,
totalPages: 1,
loading: false,
async load(page = 1, limit = 10) {
pengaduanMasyarakat.findMany.loading = true;
pengaduanMasyarakat.findMany.page = page;
try {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many"
].get({
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
}
} catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err);
} finally {
pengaduanMasyarakat.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PengaduanMasyarakatGetPayload<{
include: {
jenisPengaduan: true;
image: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/${id}`
);
if (res.ok) {
const data = await res.json();
pengaduanMasyarakat.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch pengaduan masyarakat:", res.statusText);
pengaduanMasyarakat.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching pengaduan masyarakat:", error);
pengaduanMasyarakat.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengaduanMasyarakat.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pengaduan masyarakat berhasil dihapus"
);
await pengaduanMasyarakat.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengaduan masyarakat");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengaduan masyarakat");
} finally {
pengaduanMasyarakat.delete.loading = false;
}
},
},
});
// ========================================= JENIS PENGADUAN ========================================= //
const templateJenisPengaduanForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"),
});
const defaultJenisPengaduanForm = {
nama: "",
};
const jenisPengaduan = proxy({
create: {
form: { ...defaultJenisPengaduanForm },
loading: false,
async create() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
jenisPengaduan.create.loading = true;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"create"
].post(jenisPengaduan.create.form);
if (res.status === 200) {
jenisPengaduan.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
jenisPengaduan.create.loading = false;
}
},
},
findMany: {
data: null as Array<{
id: string;
nama: string;
}> | null,
async load() {
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get();
if (res.status === 200) {
jenisPengaduan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.JenisPengaduanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${id}`
);
if (res.ok) {
const data = await res.json();
jenisPengaduan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jenisPengaduan.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
jenisPengaduan.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jenisPengaduan.delete.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Jenis pengduan berhasil dihapus");
await jenisPengaduan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus jenis pengaduan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jenis pengaduan");
} finally {
jenisPengaduan.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultJenisPengaduanForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${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 = {
nama: data.nama
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading jenis pengaduan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateJenisPengaduanForm.safeParse(jenisPengaduan.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
jenisPengaduan.edit.loading = true;
const response = await fetch(
`/api/inovasi/layananonlinedesa/pengaduanmasyarakat/jenispengaduan/${jenisPengaduan.edit.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: jenisPengaduan.edit.form.nama,
}),
}
);
// Clone the response to avoid 'body already read' error
const responseClone = response.clone();
try {
const result = await response.json();
if (!response.ok) {
console.error(
"Update failed with status:",
response.status,
"Response:",
result
);
throw new Error(
result?.message ||
`Gagal mengupdate jenis pengaduan (${response.status})`
);
}
if (result.success) {
toast.success(
result.message || "Berhasil memperbarui jenis pengaduan"
);
await jenisPengaduan.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate jenis pengaduan");
}
} catch (error) {
// If JSON parsing fails, try to get the response text for better error messages
try {
const text = await responseClone.text();
console.error("Error response text:", text);
throw new Error(`Gagal memproses respons dari server: ${text}`);
} catch (textError) {
console.error("Error parsing response as text:", textError);
console.error("Original error:", error);
throw new Error("Gagal memproses respons dari server");
}
}
} catch (error) {
console.error("Error updating jenis pengaduan:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate jenis pengaduan"
);
return false;
} finally {
jenisPengaduan.edit.loading = false;
}
},
reset() {
jenisPengaduan.edit.id = "";
jenisPengaduan.edit.form = { ...defaultJenisPengaduanForm };
},
},
});
const layananonlineDesa = proxy({
administrasiOnline,
jenisLayanan,
pengaduanMasyarakat,
jenisPengaduan,
});
export default layananonlineDesa;

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"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
jumlah: "",
icon: "",
};
const dataLingkunganDesaState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(dataLingkunganDesaState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataLingkunganDesaState.create.loading = true;
const res = await ApiFetch.api.lingkungan.datalingkungandesa["create"].post(
dataLingkunganDesaState.create.form
);
if (res.status === 200) {
dataLingkunganDesaState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
dataLingkunganDesaState.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
dataLingkunganDesaState.findMany.loading = true; // Use the full path to access the property
dataLingkunganDesaState.findMany.page = page;
try {
const res = await ApiFetch.api.lingkungan.datalingkungandesa["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
dataLingkunganDesaState.findMany.data = res.data.data || [];
dataLingkunganDesaState.findMany.total = res.data.total || 0;
dataLingkunganDesaState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load berdasarkan data lingkungan desa :",
res.data?.message
);
dataLingkunganDesaState.findMany.data = [];
dataLingkunganDesaState.findMany.total = 0;
dataLingkunganDesaState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading berdasarkan data lingkungan desa :", error);
dataLingkunganDesaState.findMany.data = [];
dataLingkunganDesaState.findMany.total = 0;
dataLingkunganDesaState.findMany.totalPages = 1;
} finally {
dataLingkunganDesaState.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/lingkungan/datalingkungandesa/${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,
jumlah: data.jumlah,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading data lingkungan desa :", 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/lingkungan/datalingkungandesa/${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 dataLingkunganDesaState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data data lingkungan desa");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.DataLingkunganDesaGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/datalingkungandesa/${id}`);
if (res.ok) {
const data = await res.json();
dataLingkunganDesaState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataLingkunganDesaState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading data lingkungan desa:", error);
dataLingkunganDesaState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataLingkunganDesaState.delete.loading = true;
const response = await fetch(`/api/lingkungan/datalingkungandesa/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data lingkungan desa berhasil dihapus");
await dataLingkunganDesaState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus data lingkungan desa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus data lingkungan desa");
} finally {
dataLingkunganDesaState.delete.loading = false;
}
},
},
});
export default dataLingkunganDesaState;

View File

@@ -0,0 +1,240 @@
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 templateTujuanEdukasiForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type TujuanEdukasiForm = Prisma.TujuanEdukasiLingkunganGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateTujuanEdukasi = proxy({
findById: {
data: null as TujuanEdukasiForm | null,
loading: false,
initialize() {
stateTujuanEdukasi.findById.data = {
id: '',
judul: '',
deskripsi: '',
} as TujuanEdukasiForm;
},
async load(id: string) {
try {
stateTujuanEdukasi.findById.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.tujuanedukasilingkungan["find-by-id"].get({
query: { id },
});
if (res.status === 200) {
stateTujuanEdukasi.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data tujuan edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data tujuan edukasi");
} finally {
stateTujuanEdukasi.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: TujuanEdukasiForm) {
const cek = templateTujuanEdukasiForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateTujuanEdukasi.update.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.tujuanedukasilingkungan["update"].post(data);
if (res.status === 200) {
toast.success("Data tujuan edukasi berhasil diubah");
await stateTujuanEdukasi.findById.load(data.id);
} else {
toast.error("Gagal mengubah data tujuan edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data tujuan edukasi");
} finally {
stateTujuanEdukasi.update.loading = false;
}
},
},
});
const templateMateriEdukasiLingkunganForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type MateriEdukasiLingkunganForm = Prisma.MateriEdukasiLingkunganGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateMateriEdukasiLingkungan = proxy({
findById: {
data: null as MateriEdukasiLingkunganForm | null,
loading: false,
initialize() {
stateMateriEdukasiLingkungan.findById.data = {
id: '',
judul: '',
deskripsi: '',
} as MateriEdukasiLingkunganForm;
},
async load(id: string) {
try {
stateMateriEdukasiLingkungan.findById.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.materiedukasilingkungan["find-by-id"].get({
query: { id },
});
if (res.status === 200) {
stateMateriEdukasiLingkungan.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data materi edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data materi edukasi");
} finally {
stateMateriEdukasiLingkungan.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: MateriEdukasiLingkunganForm) {
const cek = templateMateriEdukasiLingkunganForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateMateriEdukasiLingkungan.update.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.materiedukasilingkungan["update"].post(data);
if (res.status === 200) {
toast.success("Data materi edukasi berhasil diubah");
await stateMateriEdukasiLingkungan.findById.load(data.id);
} else {
toast.error("Gagal mengubah data materi edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data materi edukasi");
} finally {
stateMateriEdukasiLingkungan.update.loading = false;
}
},
},
});
const templateContohEdukasiLingkunganForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type ContohEdukasiLingkunganForm = Prisma.ContohEdukasiLingkunganGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateContohEdukasiLingkungan = proxy({
findById: {
data: null as ContohEdukasiLingkunganForm | null,
loading: false,
initialize() {
stateContohEdukasiLingkungan.findById.data = {
id: '',
judul: '',
deskripsi: '',
} as ContohEdukasiLingkunganForm;
},
async load(id: string) {
try {
stateContohEdukasiLingkungan.findById.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.contohkegiatandesa["find-by-id"].get({
query: { id },
});
if (res.status === 200) {
stateContohEdukasiLingkungan.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data contoh edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengambil data contoh edukasi");
} finally {
stateContohEdukasiLingkungan.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: ContohEdukasiLingkunganForm) {
const cek = templateContohEdukasiLingkunganForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateContohEdukasiLingkungan.update.loading = true;
const res = await ApiFetch.api.lingkungan.edukasilingkungan.contohkegiatandesa["update"].post(data);
if (res.status === 200) {
toast.success("Data contoh edukasi berhasil diubah");
await stateContohEdukasiLingkungan.findById.load(data.id);
} else {
toast.error("Gagal mengubah data contoh edukasi");
}
} catch (error) {
console.error((error as Error).message);
toast.error("Terjadi kesalahan saat mengubah data contoh edukasi");
} finally {
stateContohEdukasiLingkungan.update.loading = false;
}
},
},
});
const stateEdukasiLingkungan = proxy({
stateTujuanEdukasi,
stateMateriEdukasiLingkungan,
stateContohEdukasiLingkungan
})
export default stateEdukasiLingkungan;

View File

@@ -0,0 +1,274 @@
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 templateFilosofiTriHitaForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type FilosofiTriHitaForm = Prisma.FilosofiTriHitaGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateFilosofiTriHita = proxy({
findById: {
data: null as FilosofiTriHitaForm | null,
loading: false,
initialize() {
stateFilosofiTriHita.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as FilosofiTriHitaForm;
},
async load(id: string) {
try {
stateFilosofiTriHita.findById.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.filosofitrihitakarana[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateFilosofiTriHita.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data filosofi tri hita karana");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data filosofi tri hita karana"
);
} finally {
stateFilosofiTriHita.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: FilosofiTriHitaForm) {
const cek = templateFilosofiTriHitaForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateFilosofiTriHita.update.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.filosofitrihitakarana[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data filosofi tri hita karana berhasil diubah");
await stateFilosofiTriHita.findById.load(data.id);
} else {
toast.error("Gagal mengubah data filosofi tri hita karana");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data filosofi tri hita karana"
);
} finally {
stateFilosofiTriHita.update.loading = false;
}
},
},
});
const templateNilaiKonservasiAdatForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type NilaiKonservasiAdatForm = Prisma.NilaiKonservasiAdatGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateNilaiKonservasiAdat = proxy({
findById: {
data: null as NilaiKonservasiAdatForm | null,
loading: false,
initialize() {
stateNilaiKonservasiAdat.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as NilaiKonservasiAdatForm;
},
async load(id: string) {
try {
stateNilaiKonservasiAdat.findById.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.nilaikonservasiadatbali[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateNilaiKonservasiAdat.findById.data = res.data?.data ?? null;
} else {
toast.error("Gagal mengambil data nilai konservasi adat");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data nilai konservasi adat"
);
} finally {
stateNilaiKonservasiAdat.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: NilaiKonservasiAdatForm) {
const cek = templateNilaiKonservasiAdatForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateNilaiKonservasiAdat.update.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.nilaikonservasiadatbali[
"update"
].post(data);
if (res.status === 200) {
toast.success("Data nilai konservasi adat berhasil diubah");
await stateNilaiKonservasiAdat.findById.load(data.id);
} else {
toast.error("Gagal mengubah data nilai konservasi adat");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data nilai konservasi adat"
);
} finally {
stateNilaiKonservasiAdat.update.loading = false;
}
},
},
});
const templateBentukKonservasiBerdasarkanAdatForm = z.object({
judul: z.string().min(3, "Judul minimal 3 karakter"),
deskripsi: z.string().min(3, "Deskripsi minimal 3 karakter"),
});
type BentukKonservasiBerdasarkanAdatForm =
Prisma.BentukKonservasiBerdasarkanAdatGetPayload<{
select: {
id: true;
judul: true;
deskripsi: true;
};
}>;
const stateBentukKonservasiBerdasarkanAdat = proxy({
findById: {
data: null as BentukKonservasiBerdasarkanAdatForm | null,
loading: false,
initialize() {
stateBentukKonservasiBerdasarkanAdat.findById.data = {
id: "",
judul: "",
deskripsi: "",
} as BentukKonservasiBerdasarkanAdatForm;
},
async load(id: string) {
try {
stateBentukKonservasiBerdasarkanAdat.findById.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.bentukkonservasiberdasarkanadat[
"find-by-id"
].get({
query: { id },
});
if (res.status === 200) {
stateBentukKonservasiBerdasarkanAdat.findById.data =
res.data?.data ?? null;
} else {
toast.error(
"Gagal mengambil data bentuk konservasi berdasarkan adat"
);
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengambil data bentuk konservasi berdasarkan adat"
);
} finally {
stateBentukKonservasiBerdasarkanAdat.findById.loading = false;
}
},
},
update: {
loading: false,
async save(data: BentukKonservasiBerdasarkanAdatForm) {
const cek = templateBentukKonservasiBerdasarkanAdatForm.safeParse(data);
if (!cek.success) {
const errors = cek.error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join(", ");
toast.error(`Form tidak valid: ${errors}`);
return;
}
try {
stateBentukKonservasiBerdasarkanAdat.update.loading = true;
const res =
await ApiFetch.api.lingkungan.konservasiadatbali.bentukkonservasiberdasarkanadat[
"update"
].post(data);
if (res.status === 200) {
toast.success(
"Data bentuk konservasi berdasarkan adat berhasil diubah"
);
await stateBentukKonservasiBerdasarkanAdat.findById.load(data.id);
} else {
toast.error("Gagal mengubah data bentuk konservasi berdasarkan adat");
}
} catch (error) {
console.error((error as Error).message);
toast.error(
"Terjadi kesalahan saat mengubah data bentuk konservasi berdasarkan adat"
);
} finally {
stateBentukKonservasiBerdasarkanAdat.update.loading = false;
}
},
},
});
const stateKonservasiAdatBali = proxy({
stateFilosofiTriHita,
stateNilaiKonservasiAdat,
stateBentukKonservasiBerdasarkanAdat,
});
export default stateKonservasiAdatBali;

View File

@@ -0,0 +1,460 @@
/* 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"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
icon: "",
};
const pengelolaanSampah = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(pengelolaanSampah.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
pengelolaanSampah.create.loading = true;
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"create"
].post(pengelolaanSampah.create.form);
if (res.status === 200) {
pengelolaanSampah.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
pengelolaanSampah.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
pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
pengelolaanSampah.findMany.page = page;
try {
const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"find-many"
].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
pengelolaanSampah.findMany.data = res.data.data || [];
pengelolaanSampah.findMany.total = res.data.total || 0;
pengelolaanSampah.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load pengelolaan sampah:",
res.data?.message
);
pengelolaanSampah.findMany.data = [];
pengelolaanSampah.findMany.total = 0;
pengelolaanSampah.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampah.findMany.data = [];
pengelolaanSampah.findMany.total = 0;
pengelolaanSampah.findMany.totalPages = 1;
} finally {
pengelolaanSampah.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/lingkungan/pengelolaansampah/${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,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", 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/lingkungan/pengelolaansampah/${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 pengelolaanSampah.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data pengelolaan sampah");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramKreatifGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`);
if (res.ok) {
const data = await res.json();
pengelolaanSampah.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pengelolaanSampah.findUnique.data = null;
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampah.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pengelolaanSampah.delete.loading = true;
const response = await fetch(
`/api/lingkungan/pengelolaansampah/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "pengelolaan sampah berhasil dihapus"
);
await pengelolaanSampah.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus pengelolaan sampah");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah");
} finally {
pengelolaanSampah.delete.loading = false;
}
},
},
});
const templateKeteranganSampahForm = z.object({
name: z.string().min(1, "Nama minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
lat: z.number(),
lng: z.number(),
});
const defaultKeteranganSampahForm = {
name: "",
alamat: "",
namaTempatMaps: "",
lat: 0,
lng: 0,
};
const keteranganSampah = proxy({
create: {
form: { ...defaultKeteranganSampahForm },
loading: false,
async create() {
const cek = templateKeteranganSampahForm.safeParse(
keteranganSampah.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keteranganSampah.create.loading = true;
const res =
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
"create"
].post(keteranganSampah.create.form);
if (res.status === 200) {
keteranganSampah.findMany.load();
return toast.success("Data berhasil ditambahkan");
}
return toast.error("Gagal menambahkan data");
} catch (error) {
console.log(error);
toast.error("Gagal menambahkan data");
} finally {
keteranganSampah.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.KeteranganBankSampahTerdekatGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[
"find-many"
].get();
if (res.status === 200) {
keteranganSampah.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`);
if (res.ok) {
const data = await res.json();
keteranganSampah.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
keteranganSampah.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
keteranganSampah.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
keteranganSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Keterangan sampah berhasil dihapus");
await keteranganSampah.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus keterangan sampah");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus keterangan sampah");
} finally {
keteranganSampah.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultKeteranganSampahForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${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,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
};
return data;
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keterangan sampah:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKeteranganSampahForm.safeParse(keteranganSampah.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keteranganSampah.edit.loading = true;
const response = await fetch(
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
alamat: this.form.alamat,
namaTempatMaps: this.form.namaTempatMaps,
lat: this.form.lat,
lng: this.form.lng,
}),
}
);
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 keterangan sampah");
await keteranganSampah.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal mengupdate keterangan sampah");
}
} catch (error) {
console.error("Error updating keterangan sampah:", error);
toast.error(
error instanceof Error
? error.message
: "Gagal mengupdate keterangan sampah"
);
return false;
} finally {
keteranganSampah.edit.loading = false;
}
},
reset() {
keteranganSampah.edit.id = "";
keteranganSampah.edit.form = { ...defaultKeteranganSampahForm };
},
},
});
const pengelolaanSampahState = proxy({
pengelolaanSampah,
keteranganSampah,
});
export default pengelolaanSampahState;

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"),
judul: z.string().min(1, "Judul minimal 1 karakter"),
icon: z.string().min(1, "Icon minimal 1 karakter"),
});
const defaultForm = {
name: "",
deskripsi: "",
judul: "",
icon: "",
};
const programPenghijauanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async create() {
const cek = templateForm.safeParse(programPenghijauanState.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
programPenghijauanState.create.loading = true;
const res = await ApiFetch.api.lingkungan.programpenghijauan["create"].post(
programPenghijauanState.create.form
);
if (res.status === 200) {
programPenghijauanState.findMany.load();
return toast.success("success create");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
programPenghijauanState.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
programPenghijauanState.findMany.loading = true; // Use the full path to access the property
programPenghijauanState.findMany.page = page;
try {
const res = await ApiFetch.api.lingkungan.programpenghijauan["find-many"].get({
query: { page, limit },
});
if (res.status === 200 && res.data?.success) {
programPenghijauanState.findMany.data = res.data.data || [];
programPenghijauanState.findMany.total = res.data.total || 0;
programPenghijauanState.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load grafik berdasarkan program penghijauan:",
res.data?.message
);
programPenghijauanState.findMany.data = [];
programPenghijauanState.findMany.total = 0;
programPenghijauanState.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading grafik berdasarkan program penghijauan:", error);
programPenghijauanState.findMany.data = [];
programPenghijauanState.findMany.total = 0;
programPenghijauanState.findMany.totalPages = 1;
} finally {
programPenghijauanState.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/lingkungan/programpenghijauan/${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,
judul: data.judul,
icon: data.icon,
};
return data;
} else {
throw new Error(result?.message || "Gagal mengambil data");
}
} catch (error) {
console.error("Error loading program penghijauan:", 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/lingkungan/programpenghijauan/${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 programPenghijauanState.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data:", error);
toast.error("Gagal update data program penghijauan");
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.ProgramPenghijauanGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/lingkungan/programpenghijauan/${id}`);
if (res.ok) {
const data = await res.json();
programPenghijauanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
programPenghijauanState.findUnique.data = null;
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
programPenghijauanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
programPenghijauanState.delete.loading = true;
const response = await fetch(`/api/lingkungan/programpenghijauan/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Program penghijauan berhasil dihapus");
await programPenghijauanState.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus program penghijauan");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus program penghijauan");
} finally {
programPenghijauanState.delete.loading = false;
}
},
},
});
export default programPenghijauanState;

View File

@@ -25,11 +25,10 @@ function LayoutTabsLayananOnlineDesa({ children }: { children: React.ReactNode }
href: "/admin/inovasi/layanan-online-desa/pengaduan-masyarakat"
},
{
label: "Informasi Desa",
value: "informasidesa",
href: "/admin/inovasi/layanan-online-desa/informasi-desa"
label: "Jenis Pengaduan",
value: "jenispengaduan",
href: "/admin/inovasi/layanan-online-desa/jenis-pengaduan"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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 EditJenisPengaduan() {
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const state = useProxy(layananonlineDesa.jenisPengaduan);
const [formData, setFormData] = useState({
nama: "",
});
useEffect(() => {
const loadJenisPengaduan = async () => {
if (!id) return;
try {
const data = await state.edit.load(id);
if (data) {
// pastikan id-nya masuk ke state edit
state.edit.id = id;
setFormData({
nama: data.nama || '',
});
}
} catch (error) {
console.error("Error loading jenis pengaduan:", error);
toast.error("Gagal memuat data jenis pengaduan");
}
};
loadJenisPengaduan();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.nama.trim()) {
toast.error('Nama jenis pengaduan tidak boleh kosong');
return;
}
state.edit.form = {
nama: formData.nama.trim(),
};
// Safety check tambahan: pastikan ID tidak kosong
if (!state.edit.id) {
state.edit.id = id; // fallback
}
const success = await state.edit.update();
if (success) {
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan");
}
} catch (error) {
console.error("Error updating jenis pengaduan:", error);
// toast akan ditampilkan dari fungsi update
}
};
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 Pengaduan</Title>
<TextInput
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditJenisPengaduan;

View File

@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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 { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() {
const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan)
useEffect(() => {
state.findMany.load();
}, []);
const resetForm = () => {
state.create.form = {
nama: "",
};
}
const handleSubmit = async () => {
await state.create.create();
resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan")
}
return (
<Box>
<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 Pengaduan</Title>
<TextInput
value={state.create.form.nama}
onChange={(val) => {
state.create.form.nama = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>}
placeholder='Masukkan nama jenis pengaduan'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateJenisPengaduan;

View File

@@ -0,0 +1,125 @@
'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 { IconEdit, IconSearch, IconTrash } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisPengaduan() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='Jenis Pengaduan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListJenisPengaduan search={search} />
</Box>
);
}
function ListJenisPengaduan({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.jenisPengaduan)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
state.findMany.load()
}, [])
const handleHapus = async () => {
if (selectedId) {
await state.delete.byId(selectedId);
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = (state.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.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'}>
<JudulList
title='List Jenis Pengaduan'
href='/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Jenis Pengaduan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Button color="green"
onClick={() => {
if (item) {
router.push(`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`);
}
}}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus jenis pengaduan ini?"
/>
</Box>
);
}
export default JenisPengaduan;

View File

@@ -0,0 +1,124 @@
'use client'
import { useProxy } from 'valtio/utils';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors';
function DetailPengaduanMasyarakat() {
const pengaduanState = useProxy(layananonlineDesa)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter()
useShallowEffect(() => {
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat")
}
}
if (!pengaduanState.pengaduanMasyarakat.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>
<Flex gap={"xs"} justify={"space-between"} mt={10}>
<Text fz={"xl"} fw={"bold"}>Detail Pengaduan Masyarakat</Text>
<Button
onClick={() => {
if (pengaduanState.pengaduanMasyarakat.findUnique.data) {
setSelectedId(pengaduanState.pengaduanMasyarakat.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={pengaduanState.pengaduanMasyarakat.delete.loading || !pengaduanState.pengaduanMasyarakat.findUnique.data}
color={"red"}
>
<IconTrash size={20} />
</Button>
</Flex>
{pengaduanState.pengaduanMasyarakat.findUnique.data ? (
<Paper key={pengaduanState.pengaduanMasyarakat.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nomorTelepon}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Judul Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.judulPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Lokasi Kejadian</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.lokasiKejadian}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.deskripsiPengaduan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.jenisPengaduan?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text>
<Image w={{ base: 150, md: 150, lg: 150 }} src={pengaduanState.pengaduanMasyarakat.findUnique.data?.image?.link} alt="gambar" />
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?'
/>
</Box>
);
}
export default DetailPengaduanMasyarakat;

View File

@@ -0,0 +1,99 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function PengaduanMasyarakat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Pengaduan Masyarakat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPengaduanMasyarakat search={search} />
</Box>
);
}
function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat)
const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = listState.findMany;
useShallowEffect(() => {
load(page, 10);
}, [page]);
const filteredData = (data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.email.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
if (loading || !data) {
return <Skeleton h={500} />;
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Title order={3} mb={10}>List Pengaduan Masyarakat</Title>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Email</TableTh>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody style={{ overflowX: "auto" }}>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.email}</TableTd>
<TableTd>{item.nomorTelepon}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
);
}
export default PengaduanMasyarakat;

View File

@@ -9,7 +9,7 @@ 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';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
interface FormProgramKreatif {
name: string;

View File

@@ -3,7 +3,7 @@
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 { IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
@@ -25,6 +25,14 @@ function DetailProgramKreatifDesa() {
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
useShallowEffect(() => {

View File

@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../_lib/selectIcon';
import SelectIconProgram from '../../../_com/selectIcon';
function CreateProgramKreatifDesa() {

View File

@@ -0,0 +1,162 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import dataLingkunganDesaState from '@/app/admin/(dashboard)/_state/lingkungan/data-lingkungan-desa';
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 '../../../../_com/selectIconEdit';
interface FormDataLingkunganDesa {
name: string;
deskripsi: string;
jumlah: string;
icon: string;
}
type IconKey =
'ekowisata' |
'kompetisi' |
'wisata' |
'ekonomi' |
'sampah' |
'truck' |
'scale' |
'clipboard' |
'trash' |
'lingkunganSehat' |
'sumberOksigen' |
'ekonomiBerkelanjutan' |
'mencegahBencana' |
'rumah' |
'pohon' |
'air';
function EditDataLingkunganDesa() {
const stateDataLingkunganDesa = useProxy(dataLingkunganDesaState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormDataLingkunganDesa>({
name: '',
deskripsi: '',
jumlah: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateDataLingkunganDesa.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateDataLingkunganDesa.update.id = id;
stateDataLingkunganDesa.update.form = {
name: data.name,
deskripsi: data.deskripsi,
jumlah: data.jumlah,
icon: data.icon,
};
setFormData({
name: data.name,
deskripsi: data.deskripsi,
jumlah: data.jumlah,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading data lingkungan desa:", error);
toast.error("Gagal memuat data data lingkungan desa");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateDataLingkunganDesa.update.form = {
...stateDataLingkunganDesa.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
jumlah: formData.jumlah.trim(),
icon: formData.icon.trim(),
}
await stateDataLingkunganDesa.update.submit();
router.push("/admin/lingkungan/data-lingkungan-desa");
} catch (error) {
console.error("Error updating data lingkungan desa:", error);
toast.error("Gagal memuat data data lingkungan desa");
}
}
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 Data Lingkungan Desa</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<TextInput
value={formData.jumlah}
label={<Text fz={"sm"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text>}
placeholder="masukkan jumlah data lingkungan desa"
onChange={(val) => {
setFormData({
...formData,
jumlah: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateDataLingkunganDesa.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateDataLingkunganDesa.update.form.icon = value;
}} />
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditDataLingkunganDesa;

View File

@@ -0,0 +1,138 @@
/* 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, IconChristmasTreeFilled, IconClipboard, IconDroplet, IconEdit, IconHome, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTree, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailDataLingkunganDesa() {
const [modalHapus, setModalHapus] = useState(false)
const stateDataLingkungan = useProxy(dataLingkunganDesaState)
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,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet
};
useShallowEffect(() => {
stateDataLingkungan.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateDataLingkungan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/data-lingkungan-desa")
}
}
if (!stateDataLingkungan.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 Data Lingkungan Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Data Lingkungan Desa</Text>
<Text fz={"lg"}>{stateDataLingkungan.findUnique.data?.jumlah}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
{iconMap[stateDataLingkungan.findUnique.data?.icon] && (
<Box title={stateDataLingkungan.findUnique.data?.icon}>
{React.createElement(iconMap[stateDataLingkungan.findUnique.data?.icon], { size: 24 })}
</Box>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateDataLingkungan.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateDataLingkungan.findUnique.data) {
setSelectedId(stateDataLingkungan.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDataLingkungan.delete.loading || !stateDataLingkungan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateDataLingkungan.findUnique.data) {
router.push(`/admin/lingkungan/data-lingkungan-desa/${stateDataLingkungan.findUnique.data.id}/edit`);
}
}}
disabled={!stateDataLingkungan.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 data lingkungan desa ini?"
/>
</Box>
);
}
export default DetailDataLingkunganDesa;

View File

@@ -1,69 +1,68 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon';
import dataLingkunganDesaState from '../../../_state/lingkungan/data-lingkungan-desa';
function CreateDataLingkunganDesa() {
const router = useRouter()
const stateCreate = useProxy(dataLingkunganDesaState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
deskripsi: "",
jumlah: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/data-lingkungan-desa")
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}>
<Stack gap="xs">
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={3}>Create Data Lingkungan Desa</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>}
label={<Text fz={"sm"} fw={"bold"}>Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa"
/>
<TextInput
label={<Text fz="sm" fw="bold">Jumlah</Text>}
placeholder="masukkan jumlah"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
<Text fz={"sm"} fw={"bold"}>Ikon Data Lingkungan Desa</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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>
)} */}
<TextInput
type='number'
onChange={(e) => stateCreate.create.form.jumlah = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Jmlah data lingkungan desa</Text>}
placeholder='Masukkan jumlah data lingkungan desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Data Lingkungan Desa</Text>
<KeamananEditor
showSubmit={false}
<Text fw={"bold"} fz={"sm"}>Deskripsi data lingkungan desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/>
</Box>
<Button bg={colors['blue-button']} >
Simpan Data
</Button>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</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 DetailDataLingkunganDesa() {
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 Data Lingkungan Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Data Lingkungan 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"}>Jumlah</Text>
<Text fz={"lg"}>Test Jumlah</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/lingkungan/data-lingkungan-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 DetailDataLingkunganDesa;

View File

@@ -1,73 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { KeamananEditor } from '../../../keamanan/_com/keamananEditor';
function EditDataLingkunganDesa() {
const router = useRouter()
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}>Edit Data Lingkungan Desa</Title>
<TextInput
label={<Text fz="sm" fw="bold">Nama Data Lingkungan Desa</Text>}
placeholder="masukkan nama data lingkungan desa"
/>
<TextInput
label={<Text fz="sm" fw="bold">Jumlah</Text>}
placeholder="masukkan jumlah"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>}
placeholder="masukkan deskripsi"
/>
<Box>
<Text fz="sm" fw="bold">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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 fw={"bold"} fz={"sm"}>Deskripsi Data Lingkungan Desa</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Button bg={colors['blue-button']} >
Simpan Data
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditDataLingkunganDesa;

View File

@@ -1,69 +1,168 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconLeaf,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import dataLingkunganDesaState from '../../_state/lingkungan/data-lingkungan-desa';
function DataLingkunganDesa() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Data Lingkungan Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
title='Data Lingkungan Desa'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListDataLingkunganDesa/>
<ListDataLingkunganDesa search={search} />
</Box>
);
}
function ListDataLingkunganDesa() {
function ListDataLingkunganDesa({ search }: { search: string }) {
const listState = useProxy(dataLingkunganDesaState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<JudulList
title='List Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
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.jumlah.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,
truck: IconTruckFilled,
scale: IconScale,
clipboard: IconClipboardTextFilled,
trash: IconTrashFilled,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet
};
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 Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Data Lingkungan</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>No</TableTh>
<TableTh>Nama Data Lingkungan Desa</TableTh>
<TableTh>Jumlah Data Lingkungan Desa</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Lingkungan Desa</Text>
</Box>
</Table>
<Text ta="center">Tidak ada data lingkungan desa yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Data Lingkungan Desa'
href='/admin/lingkungan/data-lingkungan-desa/create'
/>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Data Lingkungan Desa</TableTh>
<TableTh style={{ width: '35%' }}>Jumlah Data Lingkungan Desa</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</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' }}>± {item.jumlah}</TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd>
<Image w={100} src={"/"} alt="image" />
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/data-lingkungan-desa/detail')}>
<IconDeviceImacCog size={25} />
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/lingkungan/data-lingkungan-desa/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
))}
</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 DataLingkunganDesa;

View File

@@ -0,0 +1,100 @@
'use client'
import { Button, Stack } from '@mantine/core';
import { Link, RichTextEditor } from '@mantine/tiptap';
import Highlight from '@tiptap/extension-highlight';
import SubScript from '@tiptap/extension-subscript';
import Superscript from '@tiptap/extension-superscript';
import TextAlign from '@tiptap/extension-text-align';
import Underline from '@tiptap/extension-underline';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect } from 'react';
export function EdukasiLingkunganTextEditor({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
onSubmit?: (val: string) => void,
onChange: (val: string) => void,
showSubmit?: boolean,
initialContent?: string }) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
immediatelyRender: false,
content: initialContent,
onUpdate : ({editor}) => {
onChange(editor.getHTML())
}
});
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
return (
<Stack >
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
{showSubmit && (
<Button onClick={() => {
if (!editor) return
onSubmit?.(editor?.getHTML())
}}>Submit</Button>
)}
</Stack>
);
}

View File

@@ -0,0 +1,67 @@
/* 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: "Tujuan Edukasi Lingkungan",
value: "tujuanedukasilingkungan",
href: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
},
{
label: "Materi Edukasi Yang Diberikan",
value: "materiedukasiyangdiberikan",
href: "/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan"
},
{
label: "Contoh Kegiatan Di Desa Darmasaba",
value: "contohkegiatan",
href: "/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba"
},
];
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}>Jumlah Penduduk Usia Kerja yang Menganggur</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,87 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
function EditContohKegiatanDesaDarmasaba() {
const router = useRouter()
const contohEdukasiState = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!contohEdukasiState.findById.data) {
contohEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (contohEdukasiState.findById.data) {
setJudul(contohEdukasiState.findById.data.judul ?? '')
setContent(contohEdukasiState.findById.data.deskripsi ?? '')
}
}, [contohEdukasiState.findById.data])
const submit = () => {
if (contohEdukasiState.findById.data) {
contohEdukasiState.findById.data.judul = judul;
contohEdukasiState.findById.data.deskripsi = content;
contohEdukasiState.update.save(contohEdukasiState.findById.data)
}
router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Contoh Kegiatan Di Desa Darmasaba</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Deskripsi</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={contohEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditContohKegiatanDesaDarmasaba;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() {
const router = useRouter()
const listContohEdukasi = useProxy(stateEdukasiLingkungan.stateContohEdukasiLingkungan)
useShallowEffect(() => {
listContohEdukasi.findById.load('edit')
}, [])
if (!listContohEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Contoh Kegiatan Di Desa Darmasaba</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/contoh-kegiatan-desa-darmasaba/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listContohEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -1,61 +0,0 @@
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function CreateEdukasiLingkungan() {
const router = useRouter()
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 Edukasi Lingkungan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Edukasi Lingkungan</Text>}
placeholder="masukkan judul edukasi lingkungan"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi Edukasi Lingkungan</Text>}
placeholder="masukkan deskripsi edukasi lingkungan"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreateEdukasiLingkungan;

View File

@@ -1,62 +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 DetailEdukasiLingkungan() {
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 Edukasi Lingkungan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Edukasi Lingkungan</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 Edukasi Lingkungan</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/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 DetailEdukasiLingkungan;

View File

@@ -1,61 +0,0 @@
'use client'
import React from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditEdukasiLingkungan() {
const router = useRouter()
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}>Edit Edukasi Lingkungan</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Edukasi Lingkungan</Text>}
placeholder="masukkan judul edukasi lingkungan"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi Edukasi Lingkungan</Text>}
placeholder="masukkan deskripsi edukasi lingkungan"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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">Gambar</Text>
<IconImageInPicture size={25} />
</Box>
<Button bg={colors['blue-button']} >
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditEdukasiLingkungan;

View File

@@ -0,0 +1,10 @@
'use client'
import LayoutTabs from "./_lib/layouTabs";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
)
}

View File

@@ -0,0 +1,87 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
function EditMateriEdukasiYangDiberikan() {
const router = useRouter()
const materiEdukasiState = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!materiEdukasiState.findById.data) {
materiEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (materiEdukasiState.findById.data) {
setJudul(materiEdukasiState.findById.data.judul ?? '')
setContent(materiEdukasiState.findById.data.deskripsi ?? '')
}
}, [materiEdukasiState.findById.data])
const submit = () => {
if (materiEdukasiState.findById.data) {
materiEdukasiState.findById.data.judul = judul;
materiEdukasiState.findById.data.deskripsi = content;
materiEdukasiState.update.save(materiEdukasiState.findById.data)
}
router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Materi Edukasi Yang Diberikan</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={materiEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditMateriEdukasiYangDiberikan;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() {
const router = useRouter()
const listMateriEdukasi = useProxy(stateEdukasiLingkungan.stateMateriEdukasiLingkungan)
useShallowEffect(() => {
listMateriEdukasi.findById.load('edit')
}, [])
if (!listMateriEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Materi Edukasi Yang Diberikan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listMateriEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -1,69 +0,0 @@
'use client'
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import React from 'react';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function Page() {
return (
<Box py={10}>
<HeaderSearch
title='Edukasi Lingkungan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListEdukasiLingkungan/>
</Box>
);
}
function ListEdukasiLingkungan() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Edukasi Lingkungan'
href='/admin/lingkungan/edukasi-lingkungan/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul Edukasi Lingkungan</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi Edukasi Lingkungan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} src="/" alt="image" />
</TableTd>
<TableTd>Deskripsi Edukasi Lingkungan</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default Page;

View File

@@ -0,0 +1,87 @@
'use client'
import stateEdukasiLingkungan from '@/app/admin/(dashboard)/_state/lingkungan/edukasi-lingkungan';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const EdukasiLingkunganTextEditor = dynamic(() => import('../../_lib/edukasiLingkunganTextEditor').then(mod => mod.EdukasiLingkunganTextEditor), {
ssr: false,
});
function EditTujuanEdukasiLingkungan() {
const router = useRouter()
const tujuanEdukasiState = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (tujuanEdukasiState.findById.data) {
setJudul(tujuanEdukasiState.findById.data.judul ?? '')
setContent(tujuanEdukasiState.findById.data.deskripsi ?? '')
}
}, [tujuanEdukasiState.findById.data])
const submit = () => {
if (tujuanEdukasiState.findById.data) {
tujuanEdukasiState.findById.data.judul = judul;
tujuanEdukasiState.findById.data.deskripsi = content;
tujuanEdukasiState.update.save(tujuanEdukasiState.findById.data)
}
router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Tujuan Edukasi Lingkungan</Title>
<Text fw={"bold"}>Judul</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<EdukasiLingkunganTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={tujuanEdukasiState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditTujuanEdukasiLingkungan;

View File

@@ -0,0 +1,54 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateEdukasiLingkungan from '../../../_state/lingkungan/edukasi-lingkungan';
function Page() {
const router = useRouter()
const listTujuanEdukasi = useProxy(stateEdukasiLingkungan.stateTujuanEdukasi)
useShallowEffect(() => {
listTujuanEdukasi.findById.load('edit')
}, [])
if (!listTujuanEdukasi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Tujuan Edukasi Lingkungan</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listTujuanEdukasi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

@@ -0,0 +1,100 @@
'use client'
import { Button, Stack } from '@mantine/core';
import { Link, RichTextEditor } from '@mantine/tiptap';
import Highlight from '@tiptap/extension-highlight';
import SubScript from '@tiptap/extension-subscript';
import Superscript from '@tiptap/extension-superscript';
import TextAlign from '@tiptap/extension-text-align';
import Underline from '@tiptap/extension-underline';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect } from 'react';
export function KonservasiAdatBaliTextEditor({ onSubmit, onChange, showSubmit = true, initialContent = '', }: {
onSubmit?: (val: string) => void,
onChange: (val: string) => void,
showSubmit?: boolean,
initialContent?: string }) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
immediatelyRender: false,
content: initialContent,
onUpdate : ({editor}) => {
onChange(editor.getHTML())
}
});
useEffect(() => {
if (editor && initialContent !== editor.getHTML()) {
editor.commands.setContent(initialContent || '<p></p>');
}
}, [initialContent, editor]);
return (
<Stack >
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset={60}>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
{showSubmit && (
<Button onClick={() => {
if (!editor) return
onSubmit?.(editor?.getHTML())
}}>Submit</Button>
)}
</Stack>
);
}

View File

@@ -0,0 +1,67 @@
/* 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: "Filosofi Tri Hita",
value: "filosofi-tri-hita",
href: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
},
{
label: "Nilai Konservasi Adat",
value: "nilai-konservasi-adat",
href: "/admin/lingkungan/konservasi-adat-bali/nilai-konservasi-adat"
},
{
label: "Bentuk Konservasi Berdasarkan Adat",
value: "bentuk-konservasi-berdasarkan-adat",
href: "/admin/lingkungan/konservasi-adat-bali/bentuk-konservasi-berdasarkan-adat"
},
];
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}>Konservasi Adat Bali</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,11 @@
import React from 'react';
function Page() {
return (
<div>
Page
</div>
);
}
export default Page;

View File

@@ -1,59 +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 CreateKonservasiAdatBali() {
const router = useRouter()
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 Konservasi Adat Bali</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Konservasi Adat Bali</Text>}
placeholder="masukkan judul konservasi adat bali"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi Konservasi Adat Bali</Text>}
placeholder="masukkan deskripsi konservasi adat bali"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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>
)} */}
<Group>
<Button bg={colors['blue-button']}>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKonservasiAdatBali;

View File

@@ -1,57 +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 DetailKonservasiAdatBali() {
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 Konservasi Adat Bali</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Konservasi Adat Bali</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Konservasi Adat Bali</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/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 DetailKonservasiAdatBali;

View File

@@ -1,59 +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 EditKonservasiAdatBali() {
const router = useRouter()
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}>Edit Konservasi Adat Bali</Title>
<TextInput
label={<Text fz="sm" fw="bold">Judul Konservasi Adat Bali</Text>}
placeholder="masukkan judul konservasi adat bali"
/>
<TextInput
label={<Text fz="sm" fw="bold">Deskripsi Konservasi Adat Bali</Text>}
placeholder="masukkan deskripsi konservasi adat bali"
/>
{/* <FileInput
label={<Text fz="sm" fw="bold">Upload Gambar</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>
)} */}
<Group>
<Button bg={colors['blue-button']}>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKonservasiAdatBali;

View File

@@ -0,0 +1,88 @@
'use client'
import stateKonservasiAdatBali from '@/app/admin/(dashboard)/_state/lingkungan/konservasi-adat-bali';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
const KonservasiAdatBaliTextEditor = dynamic(() => import('../../_lib/konservasiAdatBaliTextEditor').then(mod => mod.KonservasiAdatBaliTextEditor), {
ssr: false,
});
function EditFilosofiTriHitaKarana() {
const router = useRouter()
const filosofiTriHitaState = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita)
const [judul, setJudul] = useState('');
const [content, setContent] = useState('');
useShallowEffect(() => {
if (!filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.initialize(); // biar masuk ke `findFirst` route kamu
}
}, []);
useEffect(() => {
if (filosofiTriHitaState.findById.data) {
setJudul(filosofiTriHitaState.findById.data.judul ?? '')
setContent(filosofiTriHitaState.findById.data.deskripsi ?? '')
}
}, [filosofiTriHitaState.findById.data])
const submit = () => {
if (filosofiTriHitaState.findById.data) {
filosofiTriHitaState.findById.data.judul = judul;
filosofiTriHitaState.findById.data.deskripsi = content;
filosofiTriHitaState.update.save(filosofiTriHitaState.findById.data)
}
router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana')
}
return (
<Box>
<Stack gap={'xs'}>
<Box>
<Button
variant={'subtle'}
onClick={() => router.back()}
>
<IconArrowBack color={colors['blue-button']} size={20} />
</Button>
</Box>
<Box>
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
<Stack gap={'xs'}>
<Title order={3}>Edit Filosofi Tri Hita Karana</Title>
<Text fw={"bold"}>Judul</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setJudul}
initialContent={judul}
/>
<Text fw={"bold"}>Content</Text>
<KonservasiAdatBaliTextEditor
showSubmit={false}
onChange={setContent}
initialContent={content}
/>
<Group>
<Button
bg={colors['blue-button']}
onClick={submit}
loading={filosofiTriHitaState.update.loading}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Stack>
</Box>
);
}
export default EditFilosofiTriHitaKarana;

View File

@@ -0,0 +1,55 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Grid, GridCol, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateKonservasiAdatBali from '../../../_state/lingkungan/konservasi-adat-bali';
function Page() {
const router = useRouter()
const listFilosofi = useProxy(stateKonservasiAdatBali.stateFilosofiTriHita)
useShallowEffect(() => {
listFilosofi.findById.load('edit')
}, [])
if (!listFilosofi.findById.data) {
return (
<Stack>
<Skeleton radius={10} h={800} />
</Stack>
)
}
return (
<Paper bg={colors['white-1']} p={'md'} radius={10}>
<Stack gap={"22"}>
<Grid>
<GridCol span={{ base: 12, md: 11 }}>
<Title order={2}>Preview Filosofi Tri Hita Karana</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button bg={colors['blue-button']} onClick={() => router.push('/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana/edit')}>
<IconEdit size={16} />
</Button>
</GridCol>
</Grid>
<Box>
<Stack gap={'lg'}>
<Paper p={"xl"} bg={colors['BG-trans']}>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "h3", md: "h2" }} fw={"bold"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.judul }} />
</Box>
<Box px={{ base: 0, md: 30 }}>
<Text fz={{ base: "md", md: "h3" }} ta={"justify"} dangerouslySetInnerHTML={{ __html: listFilosofi.findById.data.deskripsi }} />
</Box>
</Paper>
</Stack>
</Box>
</Stack>
</Paper>
)
}
export default Page;

View File

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

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function KonservasiAdatBali() {
return (
<Box py={10}>
<HeaderSearch
title='Konservasi Adat Bali'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListKonservasiAdatBali />
</Box>
);
}
function ListKonservasiAdatBali() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Konservasi Adat Bali'
href='/admin/lingkungan/konservasi-adat-bali/create'
/>
<Box style={{overflowX: 'auto'}}>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}>
<TableThead>
<TableTr>
<TableTh>Judul Konservasi Adat Bali</TableTh>
<TableTh>Deskripsi Konservasi Adat Bali</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul Konservasi Adat Bali</Text>
</Box>
</TableTd>
<TableTd>Deskripsi Konservasi Adat Bali</TableTd>
<TableTd>
<Button onClick={() => router.push("/admin/lingkungan/konservasi-adat-bali/detail")}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default KonservasiAdatBali;

View File

@@ -0,0 +1,63 @@
/* 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 LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "List Pengelolaan Sampah Bank Sampah",
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"
},
];
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}>Layanan Online 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 LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -0,0 +1,141 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
const LeafletMapEdit = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapEdit'), { ssr: false });
function EditKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
const params = useParams()
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const [formData, setFormData] = useState({
name: '',
alamat: '',
namaTempatMaps: '',
lat: 0,
lng: 0,
})
useEffect(() => {
const loadKeteranganBankSampahTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await keteranganState.edit.load(id);
if (data) {
keteranganState.edit.id = id;
keteranganState.edit.form = {
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
};
setFormData({
name: data.name,
alamat: data.alamat,
namaTempatMaps: data.namaTempatMaps,
lat: data.lat,
lng: data.lng,
});
setMarkerPosition({ lat: data.lat, lng: data.lng });
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadKeteranganBankSampahTerdekat();
}, [params?.id]);
const handleSubmit = async () => {
try {
keteranganState.edit.form = {
...keteranganState.edit.form,
name: formData.name.trim(),
alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(),
lat: formData.lat,
lng: formData.lng,
}
await keteranganState.edit.update();
keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
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 Keterangan Bank Sampah Terdekat</Title>
<TextInput
value={formData.name}
onChange={(val) => setFormData({ ...formData, name: val.target.value })}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama Bank Sampah Terdekat'
/>
<TextInput
value={formData.alamat}
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })}
label={<Text fw="bold" fz="sm">Alamat</Text>}
placeholder='Masukkan alamat Bank Sampah'
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
placeholder='Masukkan nama tempat maps Bank Sampah'
/>
<Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
<Box style={{ height: 300, width: '100%' }}>
<LeafletMapEdit
key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => {
setMarkerPosition(pos);
setFormData((prev) => ({
...prev,
lat: pos.lat,
lng: pos.lng,
}));
}}
/>
</Box>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,148 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import dynamic from 'next/dynamic'
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
ssr: false
})
function DetailKeteranganBankSampahTerdekat() {
const router = useRouter();
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [modalHapus, setModalHapus] = useState(false)
const params = useParams()
useEffect(() => {
keteranganState.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
keteranganState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
}
}
if (!keteranganState.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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<Box
style={{
height: "300px",
}}
>
<LeafletMap
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
readOnly
/>
</Box>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }}
>
Buka Petunjuk Arah di Google Maps
</a>
) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text>
)}
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (keteranganState.findUnique.data) {
setSelectedId(keteranganState.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!keteranganState.findUnique.data}
color="red"
>
<IconX size={20} />
</Button>
<Button
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)}
color="green"
>
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?"
/>
</Box>
);
}
export default DetailKeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,91 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), { ssr: false });
function CreateKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
const [markerPosition, setMarkerPosition] = useState<{ lat: number; lng: number } | null>(null);
const resetForm = () => {
keteranganState.create.form = {
name: "",
alamat: "",
namaTempatMaps: "",
lat: 0,
lng: 0,
}
setMarkerPosition(null)
}
const handleSubmit = async () => {
if (markerPosition) {
keteranganState.create.form.lat = markerPosition.lat
keteranganState.create.form.lng = markerPosition.lng
}
await keteranganState.create.create()
resetForm()
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
}
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 Keterangan Bank Sampah Terdekat</Title>
<TextInput
value={keteranganState.create.form.name}
onChange={(val) => keteranganState.create.form.name = val.target.value}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama Bank Sampah Terdekat'
/>
<TextInput
value={keteranganState.create.form.alamat}
onChange={(val) => keteranganState.create.form.alamat = val.target.value}
label={<Text fw="bold" fz="sm">Alamat</Text>}
placeholder='Masukkan alamat Bank Sampah'
/>
<TextInput
value={keteranganState.create.form.namaTempatMaps}
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
placeholder='Masukkan nama tempat maps Bank Sampah'
/>
<Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
<Box style={{ height: 300, width: '100%' }}>
<LeafletMap
onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }}
/>
</Box>
</Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,90 @@
/* 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 } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { useEffect, useState } from 'react';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
function KeteranganBankSampahTerdekat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Keterangan Bank Sampah Terdekat'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeteranganBankSampahTerdekat search={search}/>
</Box>
);
}
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter();
useEffect(() => {
keteranganState.findMany.load()
}, [])
const filteredData = (keteranganState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaTempatMaps.toLowerCase().includes(keyword)
);
});
if (!keteranganState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Keterangan Bank Sampah Terdekat'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah Terdekat</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Tempat Maps</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>{item.namaTempatMaps}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default KeteranganBankSampahTerdekat;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function CreateKeteranganBankSampahTerdekat() {
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 Keterangan Bank Sampah Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama bank sampah terdekat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Bank Sampah Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKeteranganBankSampahTerdekat;

View File

@@ -1,62 +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 DetailKeteranganBankSampahTerdekat() {
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 Keterangan Bank Sampah Terdekat</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</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</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/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 DetailKeteranganBankSampahTerdekat;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function EditKeteranganBankSampahTerdekat() {
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 Keterangan Bank Sampah Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Bank Sampah Terdekat</Text>}
placeholder='Masukkan nama bank sampah terdekat'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Bank Sampah Terdekat</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditKeteranganBankSampahTerdekat;

View File

@@ -1,55 +0,0 @@
'use client'
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/judulListTab';
function KeteranganBankSampahTerdekat() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Keterangan Bank Sampah Terdekat'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Keterangan Bank Sampah Terdekat</Title>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah Terdekat</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Bank Sampah Sarana Gathi</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default KeteranganBankSampahTerdekat;

View File

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

View File

@@ -0,0 +1,115 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
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';
interface FormProgramKreatif {
name: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash';
function EditProgramKreatifDesa() {
const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({
name: '',
icon: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateSampah.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateSampah.update.id = id;
stateSampah.update.form = {
name: data.name,
icon: data.icon,
};
setFormData({
name: data.name,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateSampah.update.form = {
...stateSampah.update.form,
name: formData.name.trim(),
icon: formData.icon.trim(),
}
await stateSampah.update.submit();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
} catch (error) {
console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
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 List Pengelolaan Sampah Bank Sampah</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>}
placeholder="masukkan nama list pengelolaan sampah bank sampah"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value;
}}
/>
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,58 @@
'use client'
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
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 CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah")
}
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}>Create List Pengelolaan Sampah Bank Sampah</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>}
placeholder="masukkan nama pengelolaan sampah bank sampah"
onChange={(val) => stateCreate.create.form.name = val.target.value}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePengelolaanSampahBankSampah;

View File

@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, IconX } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
import React from 'react';
function PengelolaanSampahBankSampah() {
const [search, setSearch] = useState("")
return (
<Box>
<HeaderSearch
title='List Pengelolaan Sampah Bank Sampah'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPengelolaanSampahBankSampah search={search} />
</Box>
);
}
function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const stateList = useProxy(pengelolaanSampahState.pengelolaanSampah)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter()
useShallowEffect(() => {
stateList.findMany.load()
}, [])
const handleHapus = () => {
if (selectedId) {
stateList.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = (stateList.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.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,
truck: IconTruckFilled,
scale: IconScale,
clipboard: IconClipboardTextFilled,
trash: IconTrashFilled,
};
if (!stateList.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pengelolaan Sampah Bank Sampah'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Pengelolaan Sampah</TableTh>
<TableTh>Icon</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconX size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengelolaan sampah bank sampah ini?'
/>
</Box>
);
}
export default PengelolaanSampahBankSampah;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function CreateMekanismeBankSampah() {
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 Mekanisme Bank Sampah</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateMekanismeBankSampah;

View File

@@ -1,61 +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 DetailPengelolaanSampahBankSampah() {
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 Mekanisme Bank Sampah</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul</Text>
<Text fz={"lg"}>Test Judul</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<IconImageInPicture color={colors['blue-button']} size={25} />
</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/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/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 DetailPengelolaanSampahBankSampah;

View File

@@ -1,46 +0,0 @@
'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
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';
function EditMekanismeBankSampah() {
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 Mekanisme Bank Sampah</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Judul</Text>}
placeholder='Masukkan judul'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>
<KeamananEditor
showSubmit={false}
/>
</Box>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditMekanismeBankSampah;

View File

@@ -1,55 +0,0 @@
'use client'
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/judulListTab';
function ListPengelolaanSampahBankSampah() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<JudulListTab
title='Mekanisme Bank Sampah'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/create'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<Title order={4}>List Mekanisme Bank Sampah</Title>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list_pengelolaan_sampah_bank_sampah/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default ListPengelolaanSampahBankSampah;

View File

@@ -1,36 +0,0 @@
import { Box, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import colors from '@/con/colors';
import ListPengelolaanSampahBankSampah from './list_pengelolaan_sampah_bank_sampah/page';
import KeteranganBankSampahTerdekat from './keterangan_bank_sampah_terdekat/page';
function PengelolaanSampahBankSampah() {
return (
<Box>
<Stack>
<Title order={3}>Pengelolaan Sampah Bank Sampah</Title>
<Tabs defaultValue="list" color={colors['blue-button']} variant="pills">
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
<TabsTab value="list">
List Pengelolaan Sampah Bank Sampah
</TabsTab>
<TabsTab value="maps">
Keterangan Bank Sampah Terdekat
</TabsTab>
</TabsList>
<TabsPanel value="list">
<ListPengelolaanSampahBankSampah />
</TabsPanel>
<TabsPanel value="maps">
<KeteranganBankSampahTerdekat />
</TabsPanel>
</Tabs>
</Stack>
</Box>
)
}
export default PengelolaanSampahBankSampah;

View File

@@ -0,0 +1,147 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
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';
interface FormProgramPenghijauan {
name: string;
deskripsi: string;
judul: string;
icon: string;
}
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'truck' | 'scale' | 'clipboard' | 'trash' | 'lingkunganSehat' | 'sumberOksigen' | 'ekonomiBerkelanjutan' | 'mencegahBencana';
function EditProgramPenghijauan() {
const stateProgramPenghijauan = useProxy(programPenghijauanState)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState<FormProgramPenghijauan>({
name: '',
deskripsi: '',
judul: '',
icon: '',
})
useEffect(() => {
const loadProgramPenghijauan = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await stateProgramPenghijauan.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramPenghijauan.update.id = id;
stateProgramPenghijauan.update.form = {
name: data.name,
judul: data.judul,
deskripsi: data.deskripsi,
icon: data.icon,
};
setFormData({
name: data.name,
judul: data.judul,
deskripsi: data.deskripsi,
icon: data.icon,
});
}
} catch (error) {
console.error("Error loading program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
}
loadProgramPenghijauan();
}, [params?.id]);
const handleSubmit = async () => {
try {
stateProgramPenghijauan.update.form = {
...stateProgramPenghijauan.update.form,
name: formData.name.trim(),
deskripsi: formData.deskripsi.trim(),
judul: formData.judul.trim(),
icon: formData.icon.trim(),
}
await stateProgramPenghijauan.update.submit();
router.push("/admin/lingkungan/program-penghijauan");
} catch (error) {
console.error("Error updating program penghijauan:", error);
toast.error("Gagal memuat data program penghijauan");
}
}
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 Penghijauan</Title>
<TextInput
value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Penghijauan</Text>}
placeholder="masukkan nama program penghijauan"
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/>
<TextInput
value={formData.judul}
label={<Text fz={"sm"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text>}
placeholder="masukkan judul deskripsi program penghijauan"
onChange={(val) => {
setFormData({
...formData,
judul: val.target.value
})
}}
/>
<Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }));
stateProgramPenghijauan.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Penghijauan</Text>
<SelectIconProgramEdit
value={formData.icon as IconKey}
onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value }));
stateProgramPenghijauan.update.form.icon = value;
}} />
</Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramPenghijauan;

View File

@@ -0,0 +1,135 @@
/* 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, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramPenghijauan() {
const [modalHapus, setModalHapus] = useState(false)
const stateProgramPenghijauan = useProxy(programPenghijauanState)
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,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
useShallowEffect(() => {
stateProgramPenghijauan.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateProgramPenghijauan.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/lingkungan/program-penghijauan")
}
}
if (!stateProgramPenghijauan.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 Penghijauan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Penghijauan</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Penghijauan</Text>
{iconMap[stateProgramPenghijauan.findUnique.data?.icon] && (
<Box title={stateProgramPenghijauan.findUnique.data?.icon}>
{React.createElement(iconMap[stateProgramPenghijauan.findUnique.data?.icon], { size: 24 })}
</Box>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Judul Deskripsi Program Penghijauan</Text>
<Text fz={"lg"}>{stateProgramPenghijauan.findUnique.data?.judul}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Program Penghijauan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramPenghijauan.findUnique.data?.deskripsi }}></Text>
</Box>
<Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (stateProgramPenghijauan.findUnique.data) {
setSelectedId(stateProgramPenghijauan.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateProgramPenghijauan.delete.loading || !stateProgramPenghijauan.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (stateProgramPenghijauan.findUnique.data) {
router.push(`/admin/lingkungan/program-penghijauan/${stateProgramPenghijauan.findUnique.data.id}/edit`);
}
}}
disabled={!stateProgramPenghijauan.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 penghijauan ini?"
/>
</Box>
);
}
export default DetailProgramPenghijauan;

View File

@@ -1,53 +1,71 @@
'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 SelectIconProgram from '../../../_com/selectIcon';
import programPenghijauanState from '../../../_state/lingkungan/program-penghijauan';
function CreateProgramKreatifDesa() {
function CreateProgramPenghijauan() {
const stateCreate = useProxy(programPenghijauanState)
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
name: "",
deskripsi: "",
judul: "",
icon: "",
}
}
const handleSubmit = async () => {
await stateCreate.create.create();
resetForm();
router.push("/admin/lingkungan/program-penghijauan")
}
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 Penghijauan</Title>
<Title order={3}>Create Program Penghijauan</Title>
<TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Program Penghijauan</Text>}
placeholder="masukkan nama program penghijauan"
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 Penghijauan</Text>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Penghijauan</Text>}
placeholder='Masukkan nama program penghijauan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Penghijauan</Text>}
placeholder='Masukkan deskripsi singkat program penghijauan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
placeholder='Masukkan jumlah'
onChange={(e) => stateCreate.create.form.judul = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Judul Deskripsi Program Penghijauan</Text>}
placeholder='Masukkan judul deskripsi program penghijauan'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<KeamananEditor
showSubmit={false}
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Penghijauan</Text>
<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>
);
}
export default CreateProgramKreatifDesa;
export default CreateProgramPenghijauan;

View File

@@ -1,62 +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 DetailProgramPenghijauan() {
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 Penghijauan</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Penghijauan</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</Text>
<Text fz={"lg"} >Test Deskripsi</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/lingkungan/program-penghijauan/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 DetailProgramPenghijauan;

View File

@@ -1,53 +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 Penghijauan</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Program Penghijauan</Text>}
placeholder='Masukkan nama program penghijauan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Penghijauan</Text>}
placeholder='Masukkan deskripsi singkat program penghijauan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Jumlah</Text>}
placeholder='Masukkan jumlah'
/>
<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,68 +1,163 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import HeaderSearch from '../../_com/header';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import colors from '@/con/colors';
import JudulList from '../../_com/judulList';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import {
IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconDeviceImac, IconHomeEco, IconLeaf,
IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent,
IconTrashFilled,
IconTrendingUp,
IconTrophy,
IconTruckFilled
} from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import programPenghijauanState from '../../_state/lingkungan/program-penghijauan';
function ProgramPenghijauan() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Program Penghijauan'
placeholder='pencarian'
searchIcon={<IconSearch size={20}/>}
/>
<ListManfaatPenghijauan/>
</Box>
<Box>
<HeaderSearch
title='Program Penghijauan'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramPenghijauan search={search} />
</Box>
);
}
function ListManfaatPenghijauan() {
function ListProgramPenghijauan({ search }: { search: string }) {
const listState = useProxy(programPenghijauanState)
const { data, loading, page, totalPages, load } = listState.findMany
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<Stack>
<JudulList
title='List Manfaat Program Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
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.judul.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,
truck: IconTruckFilled,
scale: IconScale,
clipboard: IconClipboardTextFilled,
trash: IconTrashFilled,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
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 Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Program Penghijauan</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Judul Deskripsi Program Penghijauan</TableTh>
<TableTh>Ikon</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>Judul</Text>
</Box>
</TableTd>
<TableTd>
<Image w={100} alt="image" />
</Table>
<Text ta="center">Tidak ada data program penghijauan yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}>
<JudulList
title='List Program Penghijauan'
href='/admin/lingkungan/program-penghijauan/create'
/>
<Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Penghijauan</TableTh>
<TableTh style={{ width: '35%' }}>Judul Deskripsi Program Penghijauan</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr>
</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.judul }}></TableTd>
<TableTd style={{ width: '10%' }}>
{iconMap[item.icon] && (
<Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })}
</Box>
)}
</TableTd>
<TableTd>Deskripsi</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/lingkungan/program-penghijauan/detail')}>
<IconDeviceImacCog size={25} />
<TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/lingkungan/program-penghijauan/${item.id}`)}>
<IconDeviceImac size={25} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Box>
</Stack>
))}
</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 ProgramPenghijauan;

View File

@@ -308,7 +308,7 @@ export const navBar = [
{
id: "Lingkungan_1",
name: "Pengelolaan Sampah (Bank Sampah)",
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah"
path: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"
},
{
id: "Lingkungan_2",
@@ -328,12 +328,12 @@ export const navBar = [
{
id: "Lingkungan_5",
name: "Edukasi Lingkungan",
path: "/admin/lingkungan/edukasi-lingkungan"
path: "/admin/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan"
},
{
id: "Lingkungan_6",
name: "Konservasi Adat Bali",
path: "/admin/lingkungan/konservasi-adat-bali"
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
}
]
}, {

View File

@@ -0,0 +1,30 @@
import prisma from '@/lib/prisma';
export default async function beritaFindFirst() {
try {
const result = await prisma.berita.findFirst({
where: {
isActive: true, // opsional kalau kamu punya field ini
},
orderBy: {
createdAt: 'desc', // ambil yang paling terbaru
},
include: {
image: true,
kategoriBerita: true,
}
});
return {
success: true,
message: 'Berhasil ambil berita terbaru',
data: result,
};
} catch (error) {
console.error('[findFirstBerita] Error:', error);
return {
success: false,
message: 'Gagal ambil berita terbaru',
};
}
}

View File

@@ -0,0 +1,19 @@
import prisma from "@/lib/prisma";
export default async function findRecentBerita() {
const result = await prisma.berita.findMany({
orderBy: {
createdAt: "desc",
},
take: 4, // ambil 4 data terbaru
include: {
image: true,
kategoriBerita: true,
},
});
return {
success: true,
data: result,
};
}

View File

@@ -5,6 +5,8 @@ import beritaCreate from "./create";
import beritaDelete from "./del";
import beritaUpdate from "./updt";
import findBeritaById from "./find-by-id";
import beritaFindFirst from "./findFirst";
import findRecentBerita from "./findRecent";
const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
.get("/category/find-many", kategoriBeritaFindMany)
@@ -22,6 +24,8 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
kategoriBeritaId: t.Union([t.String(), t.Null()]),
}),
})
.get("/find-first", beritaFindFirst)
.get("/find-recent", findRecentBerita)
.delete("/delete/:id", beritaDelete)
.put(
"/:id",
@@ -39,5 +43,6 @@ const Berita = new Elysia({ prefix: "/berita", tags: ["Desa/Berita"] })
}),
}
);
export default Berita;

View File

@@ -0,0 +1,29 @@
import prisma from '@/lib/prisma';
export default async function pengumumanFindFirst() {
try {
const result = await prisma.pengumuman.findFirst({
where: {
isActive: true, // opsional kalau kamu punya field ini
},
orderBy: {
createdAt: 'desc', // ambil yang paling terbaru
},
include: {
CategoryPengumuman: true,
}
});
return {
success: true,
message: 'Berhasil ambil pengumuman terbaru',
data: result,
};
} catch (error) {
console.error('[findFirstPengumuman] Error:', error);
return {
success: false,
message: 'Gagal ambil pengumuman terbaru',
};
}
}

View File

@@ -0,0 +1,18 @@
import prisma from "@/lib/prisma";
export default async function pengumumanFindRecent() {
const result = await prisma.pengumuman.findMany({
orderBy: {
createdAt: "desc",
},
take: 4, // ambil 4 data terbaru
include: {
CategoryPengumuman: true,
},
});
return {
success: true,
data: result,
};
}

View File

@@ -6,6 +6,8 @@ import pengumumanCategoryFindMany from "./category";
import pengumumanDelete from "./del";
import pengumumanFindById from "./find-by-id";
import pengumumanUpdate from "./updt";
import pengumumanFindFirst from "./findFirst";
import pengumumanFindRecent from "./findRecent";
const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"] })
.get("/category/find-many", pengumumanCategoryFindMany)
@@ -20,6 +22,8 @@ const Pengumuman = new Elysia({ prefix: "/pengumuman", tags: ["Desa/Pengumuman"]
categoryPengumumanId: t.Union([t.String(), t.Null()]),
}),
})
.get("/find-first", pengumumanFindFirst)
.get("/find-recent", pengumumanFindRecent)
.put("/:id", pengumumanUpdate, {
body: t.Object({
id: t.String(),

View File

@@ -1,13 +1,13 @@
import Elysia from "elysia";
import AdministrasiOnline from "./administrasi-online";
import PengaduanMasyarakat from "./pengaduan-masyarakat";
const LayananOnlineDesa = new Elysia({
prefix: "/layananonlinedesa",
tags: ["Inovasi/Layanan Online Desa"],
})
.use(AdministrasiOnline)
.use(PengaduanMasyarakat)
}).use(AdministrasiOnline);
export default LayananOnlineDesa;

View File

@@ -0,0 +1,39 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
email: string;
nomorTelepon: string;
nik: string;
judulPengaduan: string;
lokasiKejadian: string;
deskripsiPengaduan: string;
jenisPengaduanId: string;
imageId: string;
}
export default async function pengaduanMasyarakatCreate(context: Context){
const body = context.body as FormCreate;
await prisma.pengaduanMasyarakat.create({
data: {
name: body.name,
email: body.email,
nomorTelepon: body.nomorTelepon,
nik: body.nik,
judulPengaduan: body.judulPengaduan,
lokasiKejadian: body.lokasiKejadian,
deskripsiPengaduan: body.deskripsiPengaduan,
jenisPengaduanId: body.jenisPengaduanId,
imageId: body.imageId,
}
})
return {
success: true,
message: "Success create pengaduan masyarakat",
data: {
...body,
}
}
}

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
import fs from "fs/promises";
import path from "path";
export default async function pengaduanMasyarakatDelete(context: Context) {
const id = context.params?.id as string;
const pengaduan = await prisma.pengaduanMasyarakat.findUnique({
where: {
id,
},
include: {
image: true,
jenisPengaduan: true,
},
});
if (!pengaduan) {
return {
status: 404,
body: "Pengaduan masyarakat tidak ditemukan",
};
}
if (pengaduan.image) {
try {
const filePath = path.join(pengaduan.image.path, pengaduan.image.name);
await fs.unlink(filePath);
await prisma.fileStorage.delete({
where: { id: pengaduan.image.id },
});
} catch (error) {
console.error("Gagal hapus file image:", error);
}
}
await prisma.pengaduanMasyarakat.delete({
where: { id },
});
return {
success: true,
message: "Pengaduan masyarakat berhasil dihapus",
status: 200,
};
}

View File

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

View File

@@ -0,0 +1,50 @@
import prisma from "@/lib/prisma";
export default async function pengaduanMasyarakatFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak ditemukan",
}, {status: 400});
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, {status: 400});
}
const data = await prisma.pengaduanMasyarakat.findUnique({
where: { id },
include: {
image: true,
jenisPengaduan: true,
},
});
if (!data) {
return Response.json({
success: false,
message: "Pengaduan masyarakat tidak ditemukan",
}, {status: 404});
}
return Response.json({
success: true,
message: "Success fetch pengaduan masyarakat by ID",
data,
}, {status: 200});
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil pengaduan masyarakat: " + (error instanceof Error ? error.message : 'Unknown error'),
}, {status: 500});
}
}

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