API & UI Menu Lingkungan Submenu Pengelolaan Sampah

This commit is contained in:
2025-07-18 15:01:43 +08:00
parent 7439eb7687
commit 4025771a4d
25 changed files with 1235 additions and 304 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

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

@@ -1457,3 +1457,18 @@ model PengelolaanSampah {
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) 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)
}

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,57 @@
/* 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';
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',
});
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);
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

@@ -15,12 +15,12 @@ const defaultForm = {
icon: "", icon: "",
}; };
const pengelolaanSampahState = proxy({ const pengelolaanSampah = proxy({
create: { create: {
form: { ...defaultForm }, form: { ...defaultForm },
loading: false, loading: false,
async create() { async create() {
const cek = templateForm.safeParse(pengelolaanSampahState.create.form); const cek = templateForm.safeParse(pengelolaanSampah.create.form);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
@@ -29,12 +29,12 @@ const pengelolaanSampahState = proxy({
} }
try { try {
pengelolaanSampahState.create.loading = true; pengelolaanSampah.create.loading = true;
const res = await ApiFetch.api.lingkungan.pengelolaansampah["create"].post( const res = await ApiFetch.api.lingkungan.pengelolaansampah[
pengelolaanSampahState.create.form "create"
); ].post(pengelolaanSampah.create.form);
if (res.status === 200) { if (res.status === 200) {
pengelolaanSampahState.findMany.load(); pengelolaanSampah.findMany.load();
return toast.success("success create"); return toast.success("success create");
} }
console.log(res); console.log(res);
@@ -42,7 +42,7 @@ const pengelolaanSampahState = proxy({
} catch (error) { } catch (error) {
console.log((error as Error).message); console.log((error as Error).message);
} finally { } finally {
pengelolaanSampahState.create.loading = false; pengelolaanSampah.create.loading = false;
} }
}, },
}, },
@@ -54,33 +54,35 @@ const pengelolaanSampahState = proxy({
loading: false, loading: false,
load: async (page = 1, limit = 10) => { load: async (page = 1, limit = 10) => {
// Change to arrow function // Change to arrow function
pengelolaanSampahState.findMany.loading = true; // Use the full path to access the property pengelolaanSampah.findMany.loading = true; // Use the full path to access the property
pengelolaanSampahState.findMany.page = page; pengelolaanSampah.findMany.page = page;
try { try {
const res = await ApiFetch.api.lingkungan.pengelolaansampah["find-many"].get({ const res = await ApiFetch.api.lingkungan.pengelolaansampah[
"find-many"
].get({
query: { page, limit }, query: { page, limit },
}); });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pengelolaanSampahState.findMany.data = res.data.data || []; pengelolaanSampah.findMany.data = res.data.data || [];
pengelolaanSampahState.findMany.total = res.data.total || 0; pengelolaanSampah.findMany.total = res.data.total || 0;
pengelolaanSampahState.findMany.totalPages = res.data.totalPages || 1; pengelolaanSampah.findMany.totalPages = res.data.totalPages || 1;
} else { } else {
console.error( console.error(
"Failed to load pengelolaan sampah:", "Failed to load pengelolaan sampah:",
res.data?.message res.data?.message
); );
pengelolaanSampahState.findMany.data = []; pengelolaanSampah.findMany.data = [];
pengelolaanSampahState.findMany.total = 0; pengelolaanSampah.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1; pengelolaanSampah.findMany.totalPages = 1;
} }
} catch (error) { } catch (error) {
console.error("Error loading pengelolaan sampah:", error); console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findMany.data = []; pengelolaanSampah.findMany.data = [];
pengelolaanSampahState.findMany.total = 0; pengelolaanSampah.findMany.total = 0;
pengelolaanSampahState.findMany.totalPages = 1; pengelolaanSampah.findMany.totalPages = 1;
} finally { } finally {
pengelolaanSampahState.findMany.loading = false; pengelolaanSampah.findMany.loading = false;
} }
}, },
}, },
@@ -95,12 +97,15 @@ const pengelolaanSampahState = proxy({
} }
try { try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/${id}`, { const response = await fetch(
method: "GET", `/api/lingkungan/pengelolaansampah/${id}`,
headers: { {
"Content-Type": "application/json", method: "GET",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
@@ -143,19 +148,22 @@ const pengelolaanSampahState = proxy({
} }
this.loading = true; this.loading = true;
try { try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/${id}`, { const response = await fetch(
method: "PUT", `/api/lingkungan/pengelolaansampah/${id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify(this.form), "Content-Type": "application/json",
}); },
body: JSON.stringify(this.form),
}
);
const result = await response.json(); const result = await response.json();
if (!response.ok || !result?.success) { if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data"); throw new Error(result?.message || "Gagal update data");
} }
toast.success("Berhasil update data!"); toast.success("Berhasil update data!");
await pengelolaanSampahState.findMany.load(); await pengelolaanSampah.findMany.load();
return result.data; return result.data;
} catch (error) { } catch (error) {
console.error("Error update data:", error); console.error("Error update data:", error);
@@ -174,14 +182,14 @@ const pengelolaanSampahState = proxy({
const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`); const res = await fetch(`/api/lingkungan/pengelolaansampah/${id}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
pengelolaanSampahState.findUnique.data = data.data ?? null; pengelolaanSampah.findUnique.data = data.data ?? null;
} else { } else {
console.error("Failed to fetch data", res.status, res.statusText); console.error("Failed to fetch data", res.status, res.statusText);
pengelolaanSampahState.findUnique.data = null; pengelolaanSampah.findUnique.data = null;
} }
} catch (error) { } catch (error) {
console.error("Error loading pengelolaan sampah:", error); console.error("Error loading pengelolaan sampah:", error);
pengelolaanSampahState.findUnique.data = null; pengelolaanSampah.findUnique.data = null;
} }
}, },
}, },
@@ -191,20 +199,25 @@ const pengelolaanSampahState = proxy({
if (!id) return toast.warn("ID tidak valid"); if (!id) return toast.warn("ID tidak valid");
try { try {
pengelolaanSampahState.delete.loading = true; pengelolaanSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/del/${id}`, { const response = await fetch(
method: "DELETE", `/api/lingkungan/pengelolaansampah/del/${id}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok && result?.success) { if (response.ok && result?.success) {
toast.success(result.message || "pengelolaan sampah berhasil dihapus"); toast.success(
await pengelolaanSampahState.findMany.load(); // refresh list result.message || "pengelolaan sampah berhasil dihapus"
);
await pengelolaanSampah.findMany.load(); // refresh list
} else { } else {
toast.error(result?.message || "Gagal menghapus pengelolaan sampah"); toast.error(result?.message || "Gagal menghapus pengelolaan sampah");
} }
@@ -212,10 +225,236 @@ const pengelolaanSampahState = proxy({
console.error("Gagal delete:", error); console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah"); toast.error("Terjadi kesalahan saat menghapus pengelolaan sampah");
} finally { } finally {
pengelolaanSampahState.delete.loading = false; 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; export default pengelolaanSampahState;

View File

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

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

@@ -1,14 +1,42 @@
'use client' 'use client'
import { KeamananEditor } from '@/app/admin/(dashboard)/keamanan/_com/keamananEditor';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; 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 dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation'; 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() { function CreateKeteranganBankSampahTerdekat() {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah)
const router = useRouter(); 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 ( return (
<Box> <Box>
<Box mb={10}> <Box mb={10}>
@@ -20,22 +48,38 @@ function CreateKeteranganBankSampahTerdekat() {
<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"}> <Stack gap={"xs"}>
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title> <Title order={4}>Create Keterangan Bank Sampah Terdekat</Title>
<Box>
<Text fw={"bold"} fz={"sm"}>Masukkan Image</Text>
<IconImageInPicture size={50} />
</Box>
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Bank Sampah Terdekat</Text>} value={keteranganState.create.form.name}
placeholder='Masukkan nama bank sampah terdekat' 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> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Bank Sampah Terdekat</Text> <Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text>
<KeamananEditor <Box style={{ height: 300, width: '100%' }}>
showSubmit={false} <LeafletMap
/> onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }}
/>
</Box>
</Box> </Box>
<Group> <Group>
<Button bg={colors['blue-button']}>Submit</Button> <Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>
@@ -44,3 +88,4 @@ function CreateKeteranganBankSampahTerdekat() {
} }
export default CreateKeteranganBankSampahTerdekat; 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 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/judulListTab'; 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() { 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(); 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 ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}> <JudulList
<JudulListTab title='List Keterangan Bank Sampah Terdekat'
title='Keterangan Bank Sampah Terdekat' href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create'
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan_bank_sampah_terdekat/create' />
placeholder='pencarian' <Table striped withTableBorder withRowBorders>
searchIcon={<IconSearch size={20} />} <TableThead>
/> <TableTr>
<Title order={4}>List Keterangan Bank Sampah Terdekat</Title> <TableTh>Nama Bank Sampah Terdekat</TableTh>
<Box style={{ overflowX: "auto" }}> <TableTh>Alamat</TableTh>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <TableTh>Nama Tempat Maps</TableTh>
<TableThead> <TableTh>Detail</TableTh>
<TableTr> </TableTr>
<TableTh>Nama Bank Sampah Terdekat</TableTh> </TableThead>
<TableTh>Gambar</TableTh> <TableTbody>
<TableTh>Detail</TableTh> {filteredData.map((item) => (
</TableTr> <TableTr key={item.id}>
</TableThead> <TableTd>{item.name}</TableTd>
<TableTbody> <TableTd>{item.alamat}</TableTd>
<TableTr> <TableTd>{item.namaTempatMaps}</TableTd>
<TableTd> <TableTd>
<Box w={100}> <Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}>
<Text truncate="end" fz={"sm"}>Bank Sampah Sarana Gathi</Text> <IconDeviceImac size={20} />
</Box> </Button>
</TableTd> </TableTd>
<TableTd> </TableTr>
<Image w={100} alt="image" /> ))}
</TableTd> </TableTbody>
<TableTd> </Table>
<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> </Paper>
</Box> </Box>
) );
} }
export default KeteranganBankSampahTerdekat; export default KeteranganBankSampahTerdekat;

View File

@@ -19,7 +19,7 @@ type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah' | 'tr
function EditProgramKreatifDesa() { function EditProgramKreatifDesa() {
const stateSampah = useProxy(pengelolaanSampahState) const stateSampah = useProxy(pengelolaanSampahState.pengelolaanSampah)
const params = useParams() const params = useParams()
const router = useRouter(); const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({ const [formData, setFormData] = useState<FormProgramKreatif>({

View File

@@ -11,7 +11,7 @@ import { useProxy } from 'valtio/utils';
function CreatePengelolaanSampahBankSampah() { function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState) const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah)
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {

View File

@@ -31,7 +31,7 @@ function PengelolaanSampahBankSampah() {
} }
function ListPengelolaanSampahBankSampah({ search }: { search: string }) { function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const stateList = useProxy(pengelolaanSampahState) const stateList = useProxy(pengelolaanSampahState.pengelolaanSampah)
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()

View File

@@ -4,6 +4,7 @@ import pengelolaanSampahDelete from "./del";
import pengelolaanSampahFindMany from "./findMany"; import pengelolaanSampahFindMany from "./findMany";
import pengelolaanSampahFindUnique from "./findUnique"; import pengelolaanSampahFindUnique from "./findUnique";
import pengelolaanSampahUpdate from "./updt"; import pengelolaanSampahUpdate from "./updt";
import KeteranganBankSampahTerdekat from "./keterangan-bank-sampah";
const PengelolaanSampah = new Elysia({ const PengelolaanSampah = new Elysia({
prefix: "/pengelolaansampah", prefix: "/pengelolaansampah",
@@ -33,5 +34,6 @@ const PengelolaanSampah = new Elysia({
}), }),
} }
) )
.delete("/del/:id", pengelolaanSampahDelete); .delete("/del/:id", pengelolaanSampahDelete)
.use(KeteranganBankSampahTerdekat);
export default PengelolaanSampah; export default PengelolaanSampah;

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreateKeteranganBankSampahTerdekat = {
name: string;
alamat: string;
namaTempatMaps: string;
lat: number;
lng: number;
}
export default async function keteranganBankSampahTerdekatCreate(context: Context) {
const body = context.body as FormCreateKeteranganBankSampahTerdekat;
const linkPetunjukArah = `https://www.google.com/maps/dir/?api=1&destination=${body.lat},${body.lng}`;
const created = await prisma.keteranganBankSampahTerdekat.create({
data: {
name: body.name,
alamat: body.alamat,
namaTempatMaps: body.namaTempatMaps,
lat: body.lat,
lng: body.lng,
linkPetunjukArah,
},
});
return {
success: true,
message: "Success create keterangan bank sampah terdekat",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const keteranganBankSampahTerdekatDelete = async (context: Context) => {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
const keteranganBankSampahTerdekat = await prisma.keteranganBankSampahTerdekat.findUnique({
where: { id },
});
if (!keteranganBankSampahTerdekat) {
return {
status: 404,
body: "Keterangan bank sampah terdekat tidak ditemukan",
};
}
await prisma.keteranganBankSampahTerdekat.delete({
where: { id },
});
return {
success: true,
status: 200,
message: "Keterangan bank sampah terdekat berhasil dihapus",
};
};
export default keteranganBankSampahTerdekatDelete;

View File

@@ -0,0 +1,21 @@
import prisma from "@/lib/prisma";
export default async function keteranganBankSampahTerdekatFindMany() {
try {
const data = await prisma.keteranganBankSampahTerdekat.findMany({
where: { isActive: true },
});
return {
success: true,
message: "Success fetch keterangan bank sampah terdekat",
data,
};
} catch (e) {
console.error("Find many error:", e);
return {
success: false,
message: "Failed fetch keterangan bank sampah terdekat",
};
}
}

View File

@@ -0,0 +1,39 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keteranganBankSampahTerdekatFindUnique(context: Context) {
const { id } = context.params as { id: string };
if (!id) {
return {
success: false,
message: "ID keterangan bank sampah terdekat diperlukan",
};
}
try {
const keteranganBankSampahTerdekat = await prisma.keteranganBankSampahTerdekat.findUnique({
where: { id },
});
if (!keteranganBankSampahTerdekat) {
return {
success: false,
message: "Keterangan bank sampah terdekat tidak ditemukan",
};
}
return {
success: true,
data: keteranganBankSampahTerdekat,
};
} catch (error: any) {
console.error("Error findUnique keterangan bank sampah terdekat:", error);
return {
success: false,
message: "Gagal mengambil data keterangan bank sampah terdekat",
error: error.message,
};
}
}

View File

@@ -0,0 +1,44 @@
import Elysia, { t } from "elysia";
import keteranganBankSampahTerdekatCreate from "./create";
import keteranganBankSampahTerdekatDelete from "./del";
import keteranganBankSampahTerdekatFindMany from "./findMany";
import keteranganBankSampahTerdekatFindUnique from "./findUnique";
import keteranganBankSampahTerdekatUpdate from "./updt";
const KeteranganBankSampahTerdekat = new Elysia({
prefix: "/keteranganbankterdekat",
tags: ["Lingkungan/Pengelolaan Sampah/Keterangan Bank Sampah Terdekat"],
})
.get("/find-many", keteranganBankSampahTerdekatFindMany)
.get("/:id", async (context) => {
const response = await keteranganBankSampahTerdekatFindUnique(context);
return response;
})
.post("/create", keteranganBankSampahTerdekatCreate, {
body: t.Object({
name: t.String(),
alamat: t.String(),
namaTempatMaps: t.String(),
lat: t.Number(),
lng: t.Number(),
}),
})
.put(
"/:id",
async (context) => {
const response = await keteranganBankSampahTerdekatUpdate(context);
return response;
},
{
body: t.Object({
name: t.String(),
alamat: t.String(),
namaTempatMaps: t.String(),
lat: t.Number(),
lng: t.Number(),
}),
}
)
.delete("/del/:id", keteranganBankSampahTerdekatDelete);
export default KeteranganBankSampahTerdekat;

View File

@@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdateKeteranganBankSampahTerdekat = {
id: string;
name?: string;
alamat?: string;
namaTempatMaps?: string;
lat?: number;
lng?: number;
};
export default async function keteranganBankSampahTerdekatUpdate(context: Context) {
const body = context.body as FormUpdateKeteranganBankSampahTerdekat;
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID keterangan bank sampah terdekat wajib diisi",
};
}
try {
const updateData: any = {
name: body.name,
alamat: body.alamat,
namaTempatMaps: body.namaTempatMaps,
};
if (body.lat !== undefined && body.lng !== undefined) {
updateData.lat = body.lat;
updateData.lng = body.lng;
updateData.linkPetunjukArah = `https://www.google.com/maps/dir/?api=1&destination=${body.lat},${body.lng}`;
}
const updated = await prisma.keteranganBankSampahTerdekat.update({
where: { id },
data: updateData,
});
return {
success: true,
message: "Success update keterangan bank sampah terdekat",
data: updated,
};
} catch (error) {
console.error("Update error:", error);
return {
success: false,
message: "Gagal mengupdate keterangan bank sampah terdekat",
};
}
}

View File

@@ -39,96 +39,102 @@ function InformasiDesa() {
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={10}> <Stack gap={10}>
{dataBerita && ( {dataBerita && (
<Paper shadow="md" radius="md" p="md"> <Paper shadow="md" radius="md" p="md">
<Grid>
<GridCol span={{ md: 6, base: 12 }}>
<Image
src={dataBerita.image?.link || "/fallback.jpg"}
alt={dataBerita.judul}
radius="md"
fit="cover"
height={250}
maw={600}
/>
</GridCol>
<GridCol span={{ md: 6, base: 12 }}>
<Box>
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} {dayjs(dataBerita.createdAt).fromNow()}</Text>
<Title order={1} fw="bold">{dataBerita.judul}</Title>
<Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataBerita.content }} />
</Box>
</GridCol>
</Grid>
</Paper>
)}
<Stack py={10}>
<Title order={3}>Berita Terbaru</Title>
<Grid> <Grid>
<GridCol span={{ md: 6, base: 12 }}> {stateBerita.findRecent.data.map((item) => (
<Image <GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
src={dataBerita.image?.link || "/fallback.jpg"} <Card shadow="sm" radius="md" withBorder h="100%">
alt={dataBerita.judul} <Card.Section>
radius="md" <Image
fit="cover" src={item.image?.link || "/placeholder.jpg"}
height={250} alt={item.judul}
maw={600} height={160} // gambar fix height
/> fit="cover"
</GridCol> />
<GridCol span={{ md: 6, base: 12 }}> </Card.Section>
<Box> <Stack gap="xs" mt="sm">
<Text fz="sm" c="dimmed">{dataBerita.kategoriBerita?.name} {dayjs(dataBerita.createdAt).fromNow()}</Text> <Text fw={600} lineClamp={2}>
<Title order={1} fw="bold">{dataBerita.judul}</Title> {item.judul}
<Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataBerita.content }} /> </Text>
</Box> <Text size="sm" color="dimmed" lineClamp={2}>
</GridCol> {item.deskripsi}
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
</GridCol>
))}
</Grid> </Grid>
</Paper>
)}
<Stack py={10}>
<Title order={3}>Berita Terbaru</Title>
<Grid>
{stateBerita.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%">
<Card.Section>
<Image
src={item.image?.link || "/placeholder.jpg"}
alt={item.judul}
height={160} // gambar fix height
fit="cover"
/>
</Card.Section>
<Stack gap="xs" mt="sm">
<Text fw={600} lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" color="dimmed" lineClamp={2}>
{item.deskripsi}
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
</GridCol>
))}
</Grid>
</Stack> </Stack>
<Divider color={colors['blue-button']} my="md" /> <Divider color={colors['blue-button']} my="md" />
{dataPengumuman && (
<Paper shadow="md" radius="md" p="md">
<Stack gap={"xs"}>
<Title order={1} fw="bold">{dataPengumuman.judul}</Title>
<Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} {dayjs(dataPengumuman.createdAt).fromNow()}</Text>
<Box>
<Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} />
</Box>
</Stack>
</Paper>
)}
<Stack py={10}>
<Title order={3}>Pengumuman Terbaru</Title>
<Grid> <Grid>
{statePengumuman.findRecent.data.map((item) => ( <GridCol span={{ md: 6, base: 12 }}>
<GridCol span={{ base: 12, sm: 6, md: 3 }} key={item.id}> {dataPengumuman && (
<Card shadow="sm" radius="md" withBorder h="100%"> <Paper h={"97%"} shadow="md" radius="md" p="md">
<Stack gap="xs" mt="sm"> <Stack gap={"xs"}>
<Text fw={600} lineClamp={2}> <Title order={1} fw="bold">{dataPengumuman.judul}</Title>
{item.judul} <Text fz="sm" c="dimmed">{dataPengumuman.CategoryPengumuman?.name} {dayjs(dataPengumuman.createdAt).fromNow()}</Text>
</Text> <Box>
<Text size="sm" color="dimmed" lineClamp={2}> <Text ta={"justify"} mt="xs" fz="md" dangerouslySetInnerHTML={{ __html: dataPengumuman.content }} />
{item.deskripsi} </Box>
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack> </Stack>
</Card> </Paper>
</GridCol> )}
))} </GridCol>
</Grid> <GridCol span={{ md: 6, base: 12 }}>
<Stack py={10}>
<Title order={3}>Pengumuman Terbaru</Title>
<Grid>
{statePengumuman.findRecent.data.map((item) => (
<GridCol span={{ base: 12, sm: 8, md: 6 }} key={item.id}>
<Card shadow="sm" radius="md" withBorder h="100%">
<Stack gap="xs" mt="sm">
<Text fw={600} lineClamp={2}>
{item.judul}
</Text>
<Text size="sm" color="dimmed" lineClamp={2}>
{item.deskripsi}
</Text>
<Text size="xs" c="gray">
{dayjs(item.createdAt).fromNow()}
</Text>
</Stack>
</Card>
</GridCol>
))}
</Grid>
</Stack>
</GridCol>
</Grid>
</Stack> </Stack>
</Stack>
</Box> </Box>
</Stack> </Stack>
); );