Sinkronisasi UI & API Admin - User Submenu Info Sekolah

This commit is contained in:
2025-08-29 15:20:46 +08:00
parent b6d6583e77
commit 9f9a0fb451
59 changed files with 1848 additions and 521 deletions

View File

@@ -269,7 +269,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.create.loading = true; keteranganSampah.create.loading = true;
const res = const res =
await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[ await ApiFetch.api.lingkungan.keteranganbankterdekat[
"create" "create"
].post(keteranganSampah.create.form); ].post(keteranganSampah.create.form);
if (res.status === 200) { if (res.status === 200) {
@@ -291,14 +291,47 @@ const keteranganSampah = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.lingkungan.pengelolaansampah.keteranganbankterdekat[ totalPages: 1,
"find-many" total: 0,
].get(); loading: false,
if (res.status === 200) { search: "",
keteranganSampah.findMany.data = res.data?.data ?? []; load: async (page = 1, limit = 10, search = "") => {
} // Change to arrow function
}, keteranganSampah.findMany.loading = true; // Use the full path to access the property
keteranganSampah.findMany.page = page;
keteranganSampah.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.lingkungan.keteranganbankterdekat[
"find-many"
].get({
query,
});
if (res.status === 200 && res.data?.success) {
keteranganSampah.findMany.data = res.data.data || [];
keteranganSampah.findMany.total = res.data.total || 0;
keteranganSampah.findMany.totalPages = res.data.totalPages || 1;
} else {
console.error(
"Failed to load keterangan bank sampah terdekat:",
res.data?.message
);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
}
} catch (error) {
console.error("Error loading keterangan bank sampah terdekat:", error);
keteranganSampah.findMany.data = [];
keteranganSampah.findMany.total = 0;
keteranganSampah.findMany.totalPages = 1;
} finally {
keteranganSampah.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{ data: null as Prisma.KeteranganBankSampahTerdekatGetPayload<{
@@ -306,7 +339,7 @@ const keteranganSampah = proxy({
}> | null, }> | null,
async load(id: string) { async load(id: string) {
try { try {
const res = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`); const res = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
keteranganSampah.findUnique.data = data.data ?? null; keteranganSampah.findUnique.data = data.data ?? null;
@@ -328,7 +361,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.delete.loading = true; keteranganSampah.delete.loading = true;
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/del/${id}`, { const response = await fetch(`/api/lingkungan/keteranganbankterdekat/del/${id}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -363,7 +396,7 @@ const keteranganSampah = proxy({
} }
try { try {
const response = await fetch(`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${id}`, { const response = await fetch(`/api/lingkungan/keteranganbankterdekat/${id}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -408,7 +441,7 @@ const keteranganSampah = proxy({
try { try {
keteranganSampah.edit.loading = true; keteranganSampah.edit.loading = true;
const response = await fetch( const response = await fetch(
`/api/lingkungan/pengelolaansampah/keteranganbankterdekat/${this.id}`, `/api/lingkungan/keteranganbankterdekat/${this.id}`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {

View File

@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { proxy } from "valtio"; import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
// ========================================= BEASISWA PENDAFTAR ========================================= //
const templateBeasiswaPendaftar = z.object({ const templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"), namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"), nik: z.string().min(1, "NIK harus diisi"),
@@ -76,13 +79,34 @@ const beasiswaPendaftar = proxy({
isActive: true; isActive: true;
}; };
}>[], }>[],
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[ load: async (page = 1, limit = 10, search = "") => {
"findMany" beasiswaPendaftar.findMany.loading = true; // ✅ Akses langsung via nama path
].get(); beasiswaPendaftar.findMany.page = page;
if (res.status === 200) { beasiswaPendaftar.findMany.search = search;
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
beasiswaPendaftar.findMany.data = res.data.data ?? [];
beasiswaPendaftar.findMany.totalPages = res.data.totalPages ?? 1;
} else {
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch beasiswa pendaftar paginated:", err);
beasiswaPendaftar.findMany.data = [];
beasiswaPendaftar.findMany.totalPages = 1;
} finally {
beasiswaPendaftar.findMany.loading = false;
} }
}, },
}, },
@@ -275,8 +299,260 @@ const beasiswaPendaftar = proxy({
}, },
}); });
// ========================================= KEUNGGULAN PROGRAM ========================================= //
const templateKeunggulanProgram = z.object({
judul: z.string().min(1, "Judul harus diisi"),
deskripsi: z.string().min(1, "Deskripsi harus diisi"),
});
const defaultKeunggulanProgram = {
judul: "",
deskripsi: "",
};
const keunggulanProgram = proxy({
create: {
form: { ...defaultKeunggulanProgram },
loading: false,
async create() {
const cek = templateKeunggulanProgram.safeParse(
keunggulanProgram.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
keunggulanProgram.create.loading = true;
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram[
"create"
].post(keunggulanProgram.create.form);
if (res.status === 200) {
keunggulanProgram.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
keunggulanProgram.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.KeunggulanProgramGetPayload<{
omit: {
isActive: true;
};
}>[],
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
keunggulanProgram.findMany.loading = true; // ✅ Akses langsung via nama path
keunggulanProgram.findMany.page = page;
keunggulanProgram.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.pendidikan.beasiswa.keunggulanprogram["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
keunggulanProgram.findMany.data = res.data.data ?? [];
keunggulanProgram.findMany.totalPages = res.data.totalPages ?? 1;
} else {
keunggulanProgram.findMany.data = [];
keunggulanProgram.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch keunggulan program paginated:", err);
keunggulanProgram.findMany.data = [];
keunggulanProgram.findMany.totalPages = 1;
} finally {
keunggulanProgram.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.KeunggulanProgramGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${id}`
);
if (res.ok) {
const data = await res.json();
keunggulanProgram.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
keunggulanProgram.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
keunggulanProgram.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
keunggulanProgram.delete.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Keunggulan Program berhasil dihapus");
await keunggulanProgram.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus keunggulan program");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus keunggulan program");
} finally {
keunggulanProgram.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultKeunggulanProgram },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${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 = {
judul: data.judul,
deskripsi: data.deskripsi,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading keunggulan program:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateKeunggulanProgram.safeParse(
keunggulanProgram.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
keunggulanProgram.update.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/keunggulanprogram/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
judul: this.form.judul,
deskripsi: this.form.deskripsi,
}),
}
);
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 keunggulan program");
await keunggulanProgram.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update keunggulan program");
}
} catch (error) {
console.error("Error updating keunggulan program:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update keunggulan program"
);
return false;
} finally {
keunggulanProgram.update.loading = false;
}
},
reset() {
keunggulanProgram.update.id = "";
keunggulanProgram.update.form = { ...defaultKeunggulanProgram };
},
},
});
const beasiswaDesaState = proxy({ const beasiswaDesaState = proxy({
beasiswaPendaftar, beasiswaPendaftar,
keunggulanProgram
}); });
export default beasiswaDesaState; export default beasiswaDesaState;

View File

@@ -1,63 +1,93 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconTrash, IconRecycle } from '@tabler/icons-react';
function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) { function LayoutTabsPengelolaanSampahBankSampah({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() 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 tabs = [
const curentTab = tabs.find(tab => tab.href === pathname) {
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value); label: "List Pengelolaan Sampah Bank Sampah",
value: "listpengelolaansampahbanksampah",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah",
icon: <IconTrash size={18} stroke={1.8} />,
tooltip: "Kelola data pengelolaan sampah bank sampah",
},
{
label: "Keterangan Bank Sampah Terdekat",
value: "keteranganbanksampahterdekat",
href: "/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat",
icon: <IconRecycle size={18} stroke={1.8} />,
tooltip: "Kelola data bank sampah terdekat",
},
];
const handleTabChange = (value: string | null) => { const currentTab = tabs.find(tab => tab.href === pathname);
const tab = tabs.find(t => t.value === value) const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
if (tab) {
router.push(tab.href) const handleTabChange = (value: string | null) => {
} const tab = tabs.find(t => t.value === value);
setActiveTab(value) if (tab) {
router.push(tab.href);
} }
setActiveTab(value);
};
useEffect(() => { useEffect(() => {
const match = tabs.find(tab => tab.href === pathname) const match = tabs.find(tab => tab.href === pathname);
if (match) { if (match) {
setActiveTab(match.value) setActiveTab(match.value);
} }
}, [pathname]) }, [pathname]);
return ( return (
<Stack> <Stack gap="md">
<Title order={3}>Layanan Online Desa</Title> <Title order={3} mb="sm">Pengelolaan Sampah Bank Sampah</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> <Tabs
<TabsList p={"xs"} bg={"#BBC8E7FF"}> value={activeTab}
{tabs.map((e, i) => ( onChange={handleTabChange}
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> variant="pills"
))} radius="md"
</TabsList> >
{tabs.map((e, i) => ( <TabsList>
<TabsPanel key={i} value={e.value}> {tabs.map((tab) => (
{/* Konten dummy, bisa diganti tergantung routing */} <Tooltip
<></> key={tab.value}
</TabsPanel> label={tab.tooltip}
))} position="top"
</Tabs> withArrow
{children} transitionProps={{ transition: 'pop', duration: 300 }}
</Stack> >
); <TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
padding: '10px 20px',
height: 'auto',
minHeight: 44,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
<TabsPanel
value={activeTab || ''}
pt="lg"
style={{
minHeight: '60vh',
}}
>
{children}
</TabsPanel>
</Tabs>
</Stack>
);
} }
export default LayoutTabsPengelolaanSampahBankSampah; export default LayoutTabsPengelolaanSampahBankSampah;

View File

@@ -1,8 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; 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, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -64,63 +64,97 @@ function EditKeteranganBankSampahTerdekat() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
if (!formData.name.trim()) {
return toast.error('Nama bank sampah harus diisi');
}
if (!formData.alamat.trim()) {
return toast.error('Alamat harus diisi');
}
if (!formData.namaTempatMaps.trim()) {
return toast.error('Nama tempat di Maps harus diisi');
}
if (!markerPosition) {
return toast.error('Silakan pilih lokasi di peta');
}
keteranganState.edit.form = { keteranganState.edit.form = {
...keteranganState.edit.form, ...keteranganState.edit.form,
name: formData.name.trim(), name: formData.name.trim(),
alamat: formData.alamat.trim(), alamat: formData.alamat.trim(),
namaTempatMaps: formData.namaTempatMaps.trim(), namaTempatMaps: formData.namaTempatMaps.trim(),
lat: formData.lat, lat: markerPosition.lat,
lng: formData.lng, lng: markerPosition.lng,
} };
await keteranganState.edit.update(); await keteranganState.edit.update();
toast.success('Data bank sampah berhasil diperbarui');
keteranganState.findUnique.data = null; keteranganState.findUnique.data = null;
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data bank sampah');
} }
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Edit Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={formData.name} value={formData.name}
onChange={(val) => setFormData({ ...formData, name: val.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={formData.alamat} value={formData.alamat}
onChange={(val) => setFormData({ ...formData, alamat: val.target.value })} onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={formData.namaTempatMaps} value={formData.namaTempatMaps}
onChange={(val) => setFormData({ ...formData, namaTempatMaps: val.target.value })} onChange={(e) => setFormData({ ...formData, namaTempatMaps: e.target.value })}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMapEdit <LeafletMapEdit
key={markerPosition?.lat ?? 'default'} key={markerPosition?.lat ?? 'default'}
initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }} initialPosition={markerPosition || { lat: -8.65, lng: 115.2 }}
onChange={(pos) => { onChange={(pos) => {
setMarkerPosition(pos); setMarkerPosition(pos);
setFormData((prev) => ({ setFormData(prev => ({
...prev, ...prev,
lat: pos.lat, lat: pos.lat,
lng: pos.lng, lng: pos.lng,
@@ -128,9 +162,26 @@ function EditKeteranganBankSampahTerdekat() {
}} }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -3,132 +3,148 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Anchor, Box, Button, Flex, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowLeft, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import dynamic from 'next/dynamic';
import dynamic from 'next/dynamic'
const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), { const LeafletMap = dynamic(() => import('@/app/admin/(dashboard)/_com/leafletMapCreate'), {
ssr: false ssr: false
}) });
function DetailKeteranganBankSampahTerdekat() { function DetailKeteranganBankSampahTerdekat() {
const router = useRouter(); const router = useRouter();
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const params = useParams() const params = useParams();
useEffect(() => { useEffect(() => {
keteranganState.findUnique.load(params?.id as string) keteranganState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
keteranganState.delete.byId(selectedId) keteranganState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat") router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} }
} };
if (!keteranganState.findUnique.data) { if (!keteranganState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack p="md">
<Skeleton h={500} /> <Skeleton h={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box> <Box>
<Box mb={10}> <Box mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="light"
leftSection={<IconArrowLeft size={20} />}
onClick={() => router.back()}
radius="xl"
color="blue"
>
Kembali
</Button> </Button>
</Box> </Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> <Paper
<Stack> w={{ base: "100%", md: "60%" }}
<Text fz={"xl"} fw={"bold"}>Detail Keterangan Bank Sampah Terdekat</Text> p="xl"
radius="lg"
withBorder
shadow="md"
style={{ background: colors['white-1'] }}
>
<Stack gap="lg">
<Title order={2} c="dark">
Detail Bank Sampah Terdekat
</Title>
<Paper bg={colors['BG-trans']} p={'md'}> <Paper p="lg" radius="md" withBorder >
<Stack gap={"xs"}> <Stack gap="md">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Bank Sampah Terdekat</Text> <Text fz="sm" c="dimmed">Nama Bank Sampah</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.name}</Text> <Text fz="lg" fw={600}>{keteranganState.findUnique.data?.name}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text> <Text fz="sm" c="dimmed">Alamat</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.alamat}</Text> <Text fz="lg">{keteranganState.findUnique.data?.alamat}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Tempat Maps</Text> <Text fz="sm" c="dimmed">Nama Tempat di Maps</Text>
<Text fz={"lg"}>{keteranganState.findUnique.data?.namaTempatMaps}</Text> <Text fz="lg">{keteranganState.findUnique.data?.namaTempatMaps}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Peta Lokasi</Text> <Text fz="sm" c="dimmed" mb={6}>Peta Lokasi</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<Box <Box style={{ height: "300px", borderRadius: "12px", overflow: "hidden" }}>
style={{
height: "300px",
}}
>
<LeafletMap <LeafletMap
defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }} defaultCenter={{ lat: keteranganState.findUnique.data.lat, lng: keteranganState.findUnique.data.lng }}
readOnly readOnly
/> />
</Box> </Box>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Link Petunjuk Arah</Text> <Text fz="sm" c="dimmed" mb={6}>Petunjuk Arah</Text>
{keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? ( {keteranganState.findUnique.data?.lat && keteranganState.findUnique.data?.lng ? (
<a <Anchor
href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`} href={`https://www.google.com/maps/dir/?api=1&destination=${keteranganState.findUnique.data.lat},${keteranganState.findUnique.data.lng}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }} underline="always"
c="blue"
> >
Buka Petunjuk Arah di Google Maps Buka di Google Maps
</a> </Anchor>
) : ( ) : (
<Text c="dimmed" fz="sm">Koordinat belum tersedia</Text> <Text c="dimmed" fz="sm">Belum ada koordinat</Text>
)} )}
</Box> </Box>
<Box> <Flex gap="sm" mt="md">
<Flex gap={"xs"}> <Button
<Button onClick={() => {
onClick={() => { if (keteranganState.findUnique.data) {
if (keteranganState.findUnique.data) { setSelectedId(keteranganState.findUnique.data.id);
setSelectedId(keteranganState.findUnique.data.id); setModalHapus(true);
setModalHapus(true); }
} }}
}} disabled={!keteranganState.findUnique.data}
disabled={!keteranganState.findUnique.data} leftSection={<IconTrash size={18} />}
color="red" color="red"
> radius="md"
<IconX size={20} /> variant='light'
</Button> >
<Button Hapus
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)} </Button>
color="green" <Button
> onClick={() =>
<IconEdit size={20} /> router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${keteranganState.findUnique.data?.id}/edit`)
</Button> }
</Flex> leftSection={<IconEdit size={18} />}
</Box> color="green"
radius="md"
variant='light'
>
Edit
</Button>
</Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -138,10 +154,9 @@ function DetailKeteranganBankSampahTerdekat() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus keterangan bank sampah terdekat ini?" text="Apakah Anda yakin ingin menghapus data bank sampah ini?"
/> />
</Box> </Box>
); );
} }

View File

@@ -1,9 +1,10 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; 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, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { toast } from 'react-toastify';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -28,58 +29,107 @@ function CreateKeteranganBankSampahTerdekat() {
setMarkerPosition(null) setMarkerPosition(null)
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (markerPosition) { try {
keteranganState.create.form.lat = markerPosition.lat if (!keteranganState.create.form.name) {
keteranganState.create.form.lng = markerPosition.lng return toast.error('Nama bank sampah harus diisi');
}
if (markerPosition) {
keteranganState.create.form.lat = markerPosition.lat;
keteranganState.create.form.lng = markerPosition.lng;
} else {
return toast.error('Silakan pilih lokasi di peta');
}
await keteranganState.create.create();
toast.success('Data bank sampah berhasil ditambahkan');
resetForm();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat");
} catch (error) {
console.error('Error creating bank sampah:', error);
toast.error('Gagal menambahkan data bank sampah');
} }
await keteranganState.create.create()
resetForm()
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat")
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Bank Sampah Terdekat
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Keterangan Bank Sampah Terdekat</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Bank Sampah"
placeholder="Masukkan nama bank sampah"
value={keteranganState.create.form.name} value={keteranganState.create.form.name}
onChange={(val) => keteranganState.create.form.name = val.target.value} onChange={(e) => (keteranganState.create.form.name = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Bank Sampah Terdekat</Text>} required
placeholder='Masukkan nama Bank Sampah Terdekat'
/> />
<TextInput <TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
value={keteranganState.create.form.alamat} value={keteranganState.create.form.alamat}
onChange={(val) => keteranganState.create.form.alamat = val.target.value} onChange={(e) => (keteranganState.create.form.alamat = e.target.value)}
label={<Text fw="bold" fz="sm">Alamat</Text>} required
placeholder='Masukkan alamat Bank Sampah'
/> />
<TextInput <TextInput
label="Nama Tempat di Maps"
placeholder="Masukkan nama tempat yang terdaftar di Google Maps"
value={keteranganState.create.form.namaTempatMaps} value={keteranganState.create.form.namaTempatMaps}
onChange={(val) => keteranganState.create.form.namaTempatMaps = val.target.value} onChange={(e) => (keteranganState.create.form.namaTempatMaps = e.target.value)}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>} required
placeholder='Masukkan nama tempat maps Bank Sampah'
/> />
<Box> <Box>
<Text fw="bold" fz="sm">Pilih Lokasi di Peta</Text> <Text fw="bold" fz="sm" mb={6}>
<Box style={{ height: 300, width: '100%' }}> Pilih Lokasi di Peta
</Text>
<Text fz="xs" c="dimmed" mb={4}>
Klik pada peta untuk menandai lokasi
</Text>
<Box style={{ height: 300, width: '100%', borderRadius: '8px', overflow: 'hidden' }}>
<LeafletMap <LeafletMap
onSelect={(pos) => setMarkerPosition(pos)} onSelect={(pos) => setMarkerPosition(pos)}
defaultCenter={{ lat: -8.65, lng: 115.2 }} defaultCenter={{ lat: -8.65, lng: 115.2 }}
/> />
</Box> </Box>
{markerPosition && (
<Text fz="xs" mt={4} c="green">
Lokasi dipilih: {markerPosition.lat.toFixed(6)}, {markerPosition.lng.toFixed(6)}
</Text>
)}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,14 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
function KeteranganBankSampahTerdekat() { function KeteranganBankSampahTerdekat() {
@@ -17,71 +15,124 @@ function KeteranganBankSampahTerdekat() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keterangan Bank Sampah Terdekat' title='Keterangan Bank Sampah Terdekat'
placeholder='pencarian' placeholder='Cari nama bank sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKeteranganBankSampahTerdekat search={search}/> <ListKeteranganBankSampahTerdekat search={search} />
</Box> </Box>
); );
} }
function ListKeteranganBankSampahTerdekat({ search }: { search: string }) { function ListKeteranganBankSampahTerdekat({ search }: { search: string }) {
const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah) const keteranganState = useProxy(pengelolaanSampahState.keteranganSampah);
const router = useRouter(); const router = useRouter();
useEffect(() => { const {
keteranganState.findMany.load() data,
}, []) page,
totalPages,
loading,
load,
} = keteranganState.findMany;
const filteredData = (keteranganState.findMany.data || []).filter(item => { useShallowEffect(() => {
const keyword = search.toLowerCase(); load(page, 10, search);
return ( }, [page, search]);
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaTempatMaps.toLowerCase().includes(keyword)
);
});
if (!keteranganState.findMany.data) { const filteredData = data || [];
if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Keterangan Bank Sampah Terdekat' <Title order={4}>Daftar Bank Sampah Terdekat</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create' <Tooltip label="Tambah Bank Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Bank Sampah Terdekat</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/create')}
<TableTh>Alamat</TableTh> >
<TableTh>Nama Tempat Maps</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Bank Sampah</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Nama Tempat di Maps</TableTh>
<TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.name}</TableTd> <TableTr key={item.id}>
<TableTd>{item.alamat}</TableTd> <TableTd>
<TableTd>{item.namaTempatMaps}</TableTd> <Text fw={500}>{item.name}</Text>
<TableTd> </TableTd>
<Button onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}> <TableTd>
<IconDeviceImac size={20} /> <Box w={200}>
</Button> <Text lineClamp={2} truncate="end" fz="sm">
</TableTd> {item.alamat || '-'}
</TableTr> </Text>
))} </Box>
</TableTbody> </TableTd>
</Table> <TableTd>
<Text fz="sm">
{item.namaTempatMaps || '-'}
</Text>
</TableTd>
<TableTd>
<Tooltip label="Lihat Detail" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/keterangan-bank-sampah-terdekat/${item.id}`)}
>
<IconDeviceImac size={20} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4} align="center" py="xl">
<Text c="dimmed">Tidak ada data bank sampah terdekat</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
</Box> </Box>
); );

View File

@@ -3,7 +3,7 @@
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit'; import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -65,47 +65,85 @@ function EditProgramKreatifDesa() {
...stateSampah.update.form, ...stateSampah.update.form,
name: formData.name.trim(), name: formData.name.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateSampah.update.submit(); await stateSampah.update.submit();
toast.success('Data pengelolaan sampah berhasil diperbarui!');
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah"); router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
} catch (error) { } catch (error) {
console.error("Error updating pengelolaan sampah:", error); console.error("Error updating pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pengelolaan sampah");
} }
} }
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button
</Button> variant="subtle"
</Box> onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Pengelolaan Sampah Bank Sampah
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={3}>Edit List Pengelolaan Sampah Bank Sampah</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Pengelolaan Sampah"
placeholder="Masukkan nama pengelolaan sampah"
value={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama List Pengelolaan Sampah Bank Sampah</Text>} onChange={(e) => {
placeholder="masukkan nama list pengelolaan sampah bank sampah" const value = e.target.value;
onChange={(val) => { setFormData(prev => ({
setFormData({ ...prev,
...formData, name: value
name: val.target.value }));
}) stateSampah.update.form.name = value;
}} }}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon List Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
Pilih Ikon
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
setFormData((prev) => ({ ...prev, icon: value })); setFormData(prev => ({ ...prev, icon: value }));
stateSampah.update.form.icon = value; stateSampah.update.form.icon = value;
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>

View File

@@ -1,8 +1,8 @@
'use client' 'use client';
import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '@/app/admin/(dashboard)/_state/lingkungan/pengelolaan-sampah';
import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon'; import SelectIconProgram from '@/app/admin/(dashboard)/_com/selectIcon';
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, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -11,43 +11,83 @@ import { useProxy } from 'valtio/utils';
function CreatePengelolaanSampahBankSampah() { function CreatePengelolaanSampahBankSampah() {
const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah) const stateCreate = useProxy(pengelolaanSampahState.pengelolaanSampah);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateCreate.create.form = { stateCreate.create.form = {
name: "", name: "",
icon: "", icon: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); try {
resetForm(); await stateCreate.create.create();
router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah") resetForm();
} router.push("/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah");
return ( } catch (error) {
<Box> console.error('Error creating pengelolaan sampah:', error);
<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'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={3}>Create List Pengelolaan Sampah Bank Sampah</Title> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Pengelolaan Sampah Bank Sampah
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label={<Text fz={"sm"} fw={"bold"}>Nama Pengelolaan Sampah Bank Sampah</Text>} label="Nama Pengelolaan Sampah"
placeholder="masukkan nama pengelolaan sampah bank sampah" placeholder="Masukkan nama pengelolaan sampah"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ''}
onChange={(e) => (stateCreate.create.form.name = e.target.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Pengelolaan Sampah Bank Sampah</Text> <Text fw="bold" fz="sm" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Pilih Ikon
</Text>
<SelectIconProgram
onChange={(value) => (stateCreate.create.form.icon = value)}
/>
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,26 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled, IconX } from '@tabler/icons-react'; import { IconChartLine, IconClipboardTextFilled, IconEdit, IconLeaf, IconPlus, IconRecycle, IconScale, IconSearch, IconTent, IconTrashFilled, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah'; import pengelolaanSampahState from '../../../_state/lingkungan/pengelolaan-sampah';
import React from 'react'; import React from 'react';
function PengelolaanSampahBankSampah() { function PengelolaanSampahBankSampah() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='List Pengelolaan Sampah Bank Sampah' title='List Pengelolaan Sampah Bank Sampah'
placeholder='pencarian' placeholder='Cari pengelolaan sampah...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -36,9 +34,18 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => { useShallowEffect(() => {
stateList.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
@@ -48,15 +55,9 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
} }
} }
const filteredData = (stateList.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword)
|| item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<{ size: number; style?: React.CSSProperties }>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
kompetisi: IconTrophy, kompetisi: IconTrophy,
wisata: IconTent, wisata: IconTent,
@@ -68,7 +69,7 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
trash: IconTrashFilled, trash: IconTrashFilled,
}; };
if (!stateList.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -78,49 +79,107 @@ function ListPengelolaanSampahBankSampah({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Pengelolaan Sampah Bank Sampah' <Title order={4}>Daftar Pengelolaan Sampah Bank Sampah</Title>
href='/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create' <Tooltip label="Tambah Pengelolaan Sampah" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Pengelolaan Sampah</TableTh> onClick={() => router.push('/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/create')}
<TableTh>Icon</TableTh> >
<TableTh>Edit</TableTh> Tambah Baru
<TableTh>Delete</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody>
{filteredData.map((item) => ( <Box style={{ overflowX: 'auto' }}>
<TableTr key={item.id}> <Table highlightOnHover>
<TableTd>{item.name}</TableTd> <TableThead>
<TableTd style={{ width: '10%' }}> <TableTr>
{iconMap[item.icon] && ( <TableTh>Nama Pengelolaan Sampah</TableTh>
<Box title={item.icon}> <TableTh>Icon</TableTh>
{React.createElement(iconMap[item.icon], { size: 24 })} <TableTh>Edit</TableTh>
</Box> <TableTh>Delete</TableTh>
)}
</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> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.name}</Text>
</TableTd>
<TableTd>
{iconMap[item.icon] ? (
<Tooltip label={item.icon} withArrow>
<Box>
{React.createElement(iconMap[item.icon], {
size: 24,
style: { color: colors['blue-button'] }
})}
</Box>
</Tooltip>
) : (
<Text c="dimmed" fz="sm">-</Text>
)}
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/lingkungan/pengelolaan-sampah-bank-sampah/list-pengelolaan-sampah-bank-sampah/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3} align="center" py="xl">
<Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -0,0 +1,144 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } 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 EditProgramKreatifDesa() {
const state = useProxy(beasiswaDesaState.keunggulanProgram)
const params = useParams()
const router = useRouter();
const [formData, setFormData] = useState({
judul: '',
deskripsi: '',
})
useEffect(() => {
const loadProgramKreatif = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await state.update.load(id);
if (data) {
// ⬇️ FIX PENTING: tambahkan ini
state.update.id = id;
state.update.form = {
judul: data.judul,
deskripsi: data.deskripsi,
};
setFormData({
judul: data.judul,
deskripsi: data.deskripsi,
});
}
} catch (error) {
console.error("Error loading pengelolaan sampah:", error);
toast.error("Gagal memuat data pengelolaan sampah");
}
}
loadProgramKreatif();
}, [params?.id]);
const handleSubmit = async () => {
try {
state.update.form = {
...state.update.form,
judul: formData.judul.trim(),
deskripsi: formData.deskripsi.trim(),
};
await state.update.update();
toast.success('Data keunggulan program berhasil diperbarui!');
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error("Error updating keunggulan program:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data keunggulan program");
}
}
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Keunggulan Program
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={formData.judul}
onChange={(e) => {
const value = e.target.value;
setFormData(prev => ({
...prev,
judul: value
}));
state.update.form.judul = value;
}}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor
value={state.update.form.deskripsi}
onChange={(htmlContent) => {
state.update.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditProgramKreatifDesa;

View File

@@ -0,0 +1,101 @@
'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function CreateKeunggulanProgram() {
const stateCreate = useProxy(beasiswaDesaState.keunggulanProgram);
const router = useRouter();
const resetForm = () => {
stateCreate.create.form = {
judul: "",
deskripsi: "",
};
};
const handleSubmit = async () => {
try {
await stateCreate.create.create();
resetForm();
router.push("/admin/pendidikan/beasiswa-desa/keunggulan-program");
} catch (error) {
console.error('Error creating keunggulan program:', error);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" color="blue" position="bottom">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Keunggulan Program
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Judul"
placeholder="Masukkan judul"
value={stateCreate.create.form.judul || ''}
onChange={(e) => (stateCreate.create.form.judul = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => {
stateCreate.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Group justify="right" mt="md">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateKeunggulanProgram;

View File

@@ -1,71 +1,168 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrashFilled } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function BeasiswaDesa() { function KeunggulanProgram() {
const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Keunggulan Program' title='List Keunggulan Program'
placeholder='pencarian' placeholder='Cari keunggulan program...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListBeasiswaDesa/> <ListKeunggulanProgram search={search} />
</Box> </Box>
); );
} }
function ListBeasiswaDesa() { function ListKeunggulanProgram({ search }: { search: string }) {
const router = useRouter(); const stateList = useProxy(beasiswaDesaState.keunggulanProgram)
const router = useRouter()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const {
data,
page,
totalPages,
loading,
load,
} = stateList.findMany
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const handleHapus = () => {
if (selectedId) {
stateList.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
}
}
const filteredData = data || []
if (loading || !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="lg" shadow="md" radius="md">
<Stack> <Group justify="space-between" mb="md">
<Title order={4}>List Beasiswa Desa</Title> <Title order={4}>Daftar Keunggulan Program</Title>
<Box style={{overflowX: 'auto'}}> <Tooltip label="Tambah Keunggulan Program" withArrow>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}> <Button
<TableThead> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/pendidikan/beasiswa-desa/keunggulan-program/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Nama Keunggulan Program</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500}>{item.judul}</Text>
</TableTd>
<TableTd>
<Text lineClamp={1} truncate="end" fw={500} dangerouslySetInnerHTML={{ __html: item.deskripsi }}></Text>
</TableTd>
<TableTd>
<Tooltip label="Edit" withArrow>
<Button
variant="light"
color="blue"
size="sm"
onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/keunggulan-program/${item.id}`)}
>
<IconEdit size={18} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Hapus" withArrow>
<Button
variant="light"
color="red"
size="sm"
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrashFilled size={18} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr> <TableTr>
<TableTh>Nomor</TableTh> <TableTd colSpan={3} align="center" py="xl">
<TableTh>Nama Lengkap</TableTh> <Text c="dimmed">Tidak ada data pengelolaan sampah</Text>
<TableTh>Nomor Telepon</TableTh>
<TableTh>Email</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>1</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>Email</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/beasiswa-desa/detail')}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
</TableTbody> )}
</Table> </TableTbody>
</Box> </Table>
</Stack> </Box>
{totalPages > 1 && (
<Center mt="xl">
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
siblings={1}
boundaries={1}
withEdges
/>
</Center>
)}
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus pengelolaan sampah bank sampah ini?'
/>
</Box> </Box>
) );
} }
export default BeasiswaDesa; export default KeunggulanProgram;

View File

@@ -12,22 +12,22 @@ function LayoutTabs({ children }: { children: React.ReactNode }) {
{ {
label: "Jenjang Pendidikan", label: "Jenjang Pendidikan",
value: "jenjangPendidikan", value: "jenjangPendidikan",
href: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan" href: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
}, },
{ {
label: "Lembaga", label: "Lembaga",
value: "lembaga", value: "lembaga",
href: "/admin/pendidikan/info-sekolah-paud/lembaga" href: "/admin/pendidikan/info-sekolah/lembaga"
}, },
{ {
label: "Siswa", label: "Siswa",
value: "siswa", value: "siswa",
href: "/admin/pendidikan/info-sekolah-paud/siswa" href: "/admin/pendidikan/info-sekolah/siswa"
}, },
{ {
label: "Pengajar", label: "Pengajar",
value: "pengajar", value: "pengajar",
href: "/admin/pendidikan/info-sekolah-paud/pengajar" href: "/admin/pendidikan/info-sekolah/pengajar"
}, },
]; ];
const curentTab = tabs.find(tab => tab.href === pathname) const curentTab = tabs.find(tab => tab.href === pathname)

View File

@@ -61,7 +61,7 @@ function EditJenjangPendidikan() {
const success = await stateJenjang.edit.update(); const success = await stateJenjang.edit.update();
if (success) { if (success) {
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan"); router.push("/admin/pendidikan/info-sekolah/jenjang-pendidikan");
} }
} catch (error) { } catch (error) {
console.error("Error updating jenjang pendidikan:", error); console.error("Error updating jenjang pendidikan:", error);

View File

@@ -25,7 +25,7 @@ function CreateJenjangPendidikan() {
const handleSubmit = async () => { const handleSubmit = async () => {
await stateJenjang.create.create(); await stateJenjang.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan") router.push("/admin/pendidikan/info-sekolah/jenjang-pendidikan")
} }
return ( return (

View File

@@ -66,7 +66,7 @@ function ListJenjangPendidikan({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Jenjang Pendidikan' title='List Jenjang Pendidikan'
href='/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/create' href='/admin/pendidikan/info-sekolah/jenjang-pendidikan/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -81,7 +81,7 @@ function ListJenjangPendidikan({ search }: { search: string }) {
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd> <TableTd>
<Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan/${item.id}`)}> <Button color="green" onClick={() => router.push(`/admin/pendidikan/info-sekolah/jenjang-pendidikan/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -50,7 +50,7 @@ export default function EditLembaga() {
if (result) { if (result) {
toast.success("Data berhasil diperbarui"); toast.success("Data berhasil diperbarui");
router.push('/admin/pendidikan/info-sekolah-paud/lembaga'); router.push('/admin/pendidikan/info-sekolah/lembaga');
} }
}; };

View File

@@ -29,7 +29,7 @@ function DetailLembaga() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/lembaga") router.push("/admin/pendidikan/info-sekolah/lembaga")
} }
} }
@@ -86,7 +86,7 @@ function DetailLembaga() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/lembaga/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -27,7 +27,7 @@ function CreateLembaga() {
const handleSubmit = async () => { const handleSubmit = async () => {
await stateLembaga.create.create(); await stateLembaga.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/lembaga") router.push("/admin/pendidikan/info-sekolah/lembaga")
} }
return ( return (

View File

@@ -57,7 +57,7 @@ function ListLembaga({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Lembaga' title='List Lembaga'
href='/admin/pendidikan/info-sekolah-paud/lembaga/create' href='/admin/pendidikan/info-sekolah/lembaga/create'
/> />
<Table striped withTableBorder withRowBorders> <Table striped withTableBorder withRowBorders>
<TableThead> <TableThead>
@@ -73,7 +73,7 @@ function ListLembaga({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.jenjangPendidikan?.nama}</TableTd> <TableTd>{item.jenjangPendidikan?.nama}</TableTd>
<TableTd> <TableTd>
<Button color="blue" onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/lembaga/${item.id}`)}> <Button color="blue" onClick={() => router.push(`/admin/pendidikan/info-sekolah/lembaga/${item.id}`)}>
<IconDeviceImac size={20} /> <IconDeviceImac size={20} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -55,7 +55,7 @@ function EditPengajar() {
lembagaId: formData.lembagaId.trim(), lembagaId: formData.lembagaId.trim(),
} }
await pengajarState.edit.update() await pengajarState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/pengajar"); router.push("/admin/pendidikan/info-sekolah/pengajar");
} catch (error) { } catch (error) {
console.error("Error updating pengajar:", error); console.error("Error updating pengajar:", error);
toast.error("Terjadi kesalahan saat memperbarui pengajar"); toast.error("Terjadi kesalahan saat memperbarui pengajar");

View File

@@ -29,7 +29,7 @@ function DetailPengajar() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/pengajar") router.push("/admin/pendidikan/info-sekolah/pengajar")
} }
} }
@@ -78,7 +78,7 @@ function DetailPengajar() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/pengajar/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -28,7 +28,7 @@ function CreatePengajar() {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/pengajar") router.push("/admin/pendidikan/info-sekolah/pengajar")
} }
return ( return (
<Box> <Box>

View File

@@ -55,7 +55,7 @@ function ListPengajar({ search }: { search: string }) {
<Stack> <Stack>
<JudulList <JudulList
title='List Pengajar' title='List Pengajar'
href='/admin/pendidikan/info-sekolah-paud/pengajar/create' href='/admin/pendidikan/info-sekolah/pengajar/create'
/> />
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
@@ -72,7 +72,7 @@ function ListPengajar({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd> <TableTd>{item.lembaga.nama}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/pengajar/${item.id}`)}> <Button onClick={() => router.push(`/admin/pendidikan/info-sekolah/pengajar/${item.id}`)}>
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={25} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -55,7 +55,7 @@ function EditSiswa() {
lembagaId: formData.lembagaId.trim(), lembagaId: formData.lembagaId.trim(),
} }
await siswaState.edit.update() await siswaState.edit.update()
router.push("/admin/pendidikan/info-sekolah-paud/siswa"); router.push("/admin/pendidikan/info-sekolah/siswa");
} catch (error) { } catch (error) {
console.error("Error updating siswa:", error); console.error("Error updating siswa:", error);
toast.error("Terjadi kesalahan saat memperbarui siswa"); toast.error("Terjadi kesalahan saat memperbarui siswa");

View File

@@ -29,7 +29,7 @@ function DetailSiswa() {
detailState.delete.byId(selectedId) detailState.delete.byId(selectedId)
setModalHapus(false) setModalHapus(false)
setSelectedId(null) setSelectedId(null)
router.push("/admin/pendidikan/info-sekolah-paud/siswa") router.push("/admin/pendidikan/info-sekolah/siswa")
} }
} }
@@ -78,7 +78,7 @@ function DetailSiswa() {
<Button <Button
onClick={() => { onClick={() => {
if (detailState.findUnique.data) { if (detailState.findUnique.data) {
router.push(`/admin/pendidikan/info-sekolah-paud/siswa/${detailState.findUnique.data.id}/edit`); router.push(`/admin/pendidikan/info-sekolah/siswa/${detailState.findUnique.data.id}/edit`);
} }
}} }}
disabled={!detailState.findUnique.data} disabled={!detailState.findUnique.data}

View File

@@ -28,7 +28,7 @@ function CreateSiswa() {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/pendidikan/info-sekolah-paud/siswa") router.push("/admin/pendidikan/info-sekolah/siswa")
} }
return ( return (
<Box> <Box>

View File

@@ -55,7 +55,7 @@ function ListSiswa({ search }: { search: string }) {
<Stack> <Stack>
<JudulList <JudulList
title='List Siswa' title='List Siswa'
href='/admin/pendidikan/info-sekolah-paud/siswa/create' href='/admin/pendidikan/info-sekolah/siswa/create'
/> />
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
@@ -72,7 +72,7 @@ function ListSiswa({ search }: { search: string }) {
<TableTd>{item.nama}</TableTd> <TableTd>{item.nama}</TableTd>
<TableTd>{item.lembaga.nama}</TableTd> <TableTd>{item.lembaga.nama}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/info-sekolah-paud/siswa/${item.id}`)}> <Button onClick={() => router.push(`/admin/pendidikan/info-sekolah/siswa/${item.id}`)}>
<IconDeviceImacCog size={25} /> <IconDeviceImacCog size={25} />
</Button> </Button>
</TableTd> </TableTd>

View File

@@ -343,8 +343,8 @@ export const navBar = [
children: [ children: [
{ {
id: "Pendidikan_1", id: "Pendidikan_1",
name: "Info Sekolah & PAUD", name: "Info Sekolah",
path: "/admin/pendidikan/info-sekolah-paud/jenjang-pendidikan" path: "/admin/pendidikan/info-sekolah/jenjang-pendidikan"
}, },
{ {
id: "Pendidikan_2", id: "Pendidikan_2",

View File

@@ -6,6 +6,7 @@ import EdukasiLingkungan from "./edukasi-lingkungan";
import KonservasiAdatBali from "./konservasi-adat-bali"; import KonservasiAdatBali from "./konservasi-adat-bali";
import KegiatanDesa from "./gotong-royong"; import KegiatanDesa from "./gotong-royong";
import KategoriKegiatan from "./gotong-royong/kategori-kegiatan"; import KategoriKegiatan from "./gotong-royong/kategori-kegiatan";
import KeteranganBankSampahTerdekat from "./pengelolaan-sampah/keterangan-bank-sampah";
const Lingkungan = new Elysia({ const Lingkungan = new Elysia({
prefix: "/api/lingkungan", prefix: "/api/lingkungan",
@@ -19,6 +20,7 @@ const Lingkungan = new Elysia({
.use(KonservasiAdatBali) .use(KonservasiAdatBali)
.use(KegiatanDesa) .use(KegiatanDesa)
.use(KategoriKegiatan) .use(KategoriKegiatan)
.use(KeteranganBankSampahTerdekat);
export default Lingkungan; export default Lingkungan;

View File

@@ -4,7 +4,6 @@ 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",
@@ -35,5 +34,4 @@ const PengelolaanSampah = new Elysia({
} }
) )
.delete("/del/:id", pengelolaanSampahDelete) .delete("/del/:id", pengelolaanSampahDelete)
.use(KeteranganBankSampahTerdekat);
export default PengelolaanSampah; export default PengelolaanSampah;

View File

@@ -1,21 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keteranganBankSampahTerdekatFindMany() { // Di findMany.ts
try { export default async function keteranganBankSampahTerdekatFindMany(context: Context) {
const data = await prisma.keteranganBankSampahTerdekat.findMany({ const page = Number(context.query.page) || 1;
where: { isActive: true }, const limit = Number(context.query.limit) || 10;
}); const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { const where: any = { isActive: true };
success: true,
message: "Success fetch keterangan bank sampah terdekat", // Tambahkan pencarian (jika ada)
data, if (search) {
}; where.OR = [
} catch (e) { { name: { contains: search, mode: 'insensitive' } },
console.error("Find many error:", e); ];
return { }
success: false,
message: "Failed fetch keterangan bank sampah terdekat", try {
}; const [data, total] = await Promise.all([
} prisma.keteranganBankSampahTerdekat.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.keteranganBankSampahTerdekat.count({
where,
})
]);
const totalPages = Math.ceil(total / limit);
return {
success: true,
message: "Success fetch keterangan bank sampah terdekat with pagination",
data,
page,
totalPages,
total,
};
} catch (e) {
console.error("Find many paginated error:", e);
return {
success: false,
message: "Failed fetch keterangan bank sampah terdekat with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
};
}
} }

View File

@@ -1,11 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function beasiswaPendaftarFindMany() { async function beasiswaPendaftarFindMany(context: Context) {
const data = await prisma.beasiswaPendaftar.findMany(); // Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
return { // Buat where clause
success: true, const where: any = { isActive: true };
message: "Success get all beasiswa pendaftar",
data, // Tambahkan pencarian (jika ada)
}; if (search) {
where.OR = [
{ namaLengkap: { contains: search, mode: 'insensitive' } },
{ tempatLahir: { contains: search, mode: 'insensitive' } },
{ alamatKTP: { contains: search, mode: 'insensitive' } },
{ alamatDomisili: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.beasiswaPendaftar.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.beasiswaPendaftar.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil beasiswa pendaftar dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data beasiswa pendaftar",
};
}
} }
export default beasiswaPendaftarFindMany;

View File

@@ -1,10 +1,12 @@
import Elysia from "elysia"; import Elysia from "elysia";
import BeasiswaPendaftar from "./beasiswa-pendaftar"; import BeasiswaPendaftar from "./beasiswa-pendaftar";
import KeunggulanProgram from "./keunggulan-program";
const Beasiswa = new Elysia({ const Beasiswa = new Elysia({
prefix: "/beasiswa", prefix: "/beasiswa",
tags: ["Pendidikan/Beasiswa Desa"] tags: ["Pendidikan/Beasiswa Desa"]
}) })
.use(BeasiswaPendaftar) .use(BeasiswaPendaftar)
.use(KeunggulanProgram)
export default Beasiswa export default Beasiswa

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
judul: string;
deskripsi: string;
}
export default async function keunggulanProgramCreate(context: Context) {
const body = context.body as FormCreate;
try {
const result = await prisma.keunggulanProgram.create({
data: {
judul: body.judul,
deskripsi: body.deskripsi,
},
});
return {
success: true,
message: "Berhasil membuat data keunggulan program",
data: result,
};
} catch (error) {
console.error("Gagal membuat data keunggulan program:", error);
throw new Error("Gagal membuat data keunggulan program: " + (error as Error).message);
}
}

View File

@@ -0,0 +1,16 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function keunggulanProgramDelete(context: Context) {
const id = context.params.id as string;
await prisma.keunggulanProgram.delete({
where: { id },
});
return {
status: 200,
success: true,
message: "Success delete keunggulan program",
};
}

View File

@@ -0,0 +1,54 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma";
import { Context } from "elysia";
async function keunggulanProgramFindMany(context: Context) {
// Ambil parameter dari query
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ judul: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.keunggulanProgram.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.keunggulanProgram.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil keunggulan program dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data keunggulan program",
};
}
}
export default keunggulanProgramFindMany;

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function keunggulanProgramFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return {
success: false,
message: "ID is required",
}
}
try {
if (typeof id !== 'string') {
return {
success: false,
message: "ID is required",
}
}
const data = await prisma.keunggulanProgram.findUnique({
where: { id },
});
if (!data) {
return {
success: false,
message: "Data not found",
}
}
return {
success: true,
message: "Success get keunggulan program",
data,
}
} catch (error) {
console.error("Find by ID error:", error);
return {
success: false,
message: "Gagal mengambil data: " + (error instanceof Error ? error.message : 'Unknown error'),
}
}
}

View File

@@ -0,0 +1,41 @@
import Elysia, { t } from "elysia";
import keunggulanProgramCreate from "./create";
import keunggulanProgramFindMany from "./findMany";
import keunggulanProgramFindUnique from "./findUnique";
import keunggulanProgramUpdate from "./updt";
import keunggulanProgramDelete from "./del";
const KeunggulanProgram = new Elysia({
prefix: "/keunggulanprogram",
tags: ["Pendidikan / Beasiswa Desa / Keunggulan Program"],
})
.post("/create", keunggulanProgramCreate, {
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
}),
})
.get("/findMany", keunggulanProgramFindMany)
.get("/:id", async (context) => {
const response = await keunggulanProgramFindUnique(
new Request(context.request)
);
return response;
})
.put(
"/:id",
async (context) => {
const response = await keunggulanProgramUpdate(context);
return response;
},
{
body: t.Object({
judul: t.String(),
deskripsi: t.String(),
}),
}
)
.delete("/del/:id", keunggulanProgramDelete);
export default KeunggulanProgram;

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
judul: string;
deskripsi: string;
}
export default async function keunggulanProramUpdate(context: Context){
const id = context.params.id as string;
const body = context.body as FormUpdate;
try {
if (typeof id !== "string") {
return {
success: false,
message: "ID is required",
};
}
const data = await prisma.keunggulanProgram.update({
where: { id },
data: {
judul: body.judul,
deskripsi: body.deskripsi,
},
});
if (!data) {
return {
success: false,
message: "Data not found",
};
}
return {
success: true,
message: "Success update keunggulan program",
data,
};
} catch (error) {
console.error("Gagal update data keunggulan program:", error);
throw new Error("Gagal update data keunggulan program: " + (error as Error).message);
}
}

View File

@@ -87,9 +87,9 @@ function LayoutTabsGotongRoyong({
href: "/darmasaba/lingkungan/gotong-royong/kebersihan" href: "/darmasaba/lingkungan/gotong-royong/kebersihan"
}, },
{ {
label: "Infrasturktur", label: "Infrastruktur",
value: "infrasturktur", value: "infrastruktur",
href: "/darmasaba/lingkungan/gotong-royong/infrasturktur" href: "/darmasaba/lingkungan/gotong-royong/infrastruktur"
}, },
{ {
label: "Sosial", label: "Sosial",

View File

@@ -81,8 +81,9 @@ function Page() {
<Box key={k} px={28}> <Box key={k} px={28}>
<Paper p={20} bg={colors['white-trans-1']}> <Paper p={20} bg={colors['white-trans-1']}>
<Flex gap={20} align={'center'}> <Flex gap={20} align={'center'}>
<Text>{k + 1}</Text>
<Box style={{ alignContent: 'center', alignItems: 'center' }}> <Box style={{ alignContent: 'center', alignItems: 'center' }}>
{k + 1} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null} {iconMap[v.icon] ? React.createElement(iconMap[v.icon]) : null}
</Box> </Box>
<Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text> <Text fw={'bold'} fz={{ base: "lg", md: "xl" }} c={'black'}>{v.name}</Text>
</Flex> </Flex>

View File

@@ -1,14 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { IconSearch, IconLeaf, IconTrophy, IconTent, IconChartLine, IconRecycle, IconTruckFilled, IconScale, IconClipboardTextFilled, IconTrashFilled, IconHomeEco, IconChristmasTreeFilled, IconTrendingUp, IconShieldFilled } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto';
import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan'; import programPenghijauanState from '@/app/admin/(dashboard)/_state/lingkungan/program-penghijauan';
import { useProxy } from 'valtio/utils'; import colors from '@/con/colors';
import { Box, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { useState } from 'react'; import { IconChartLine, IconChristmasTreeFilled, IconClipboardTextFilled, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconSearch, IconShieldFilled, IconTent, IconTrashFilled, IconTrendingUp, IconTrophy, IconTruckFilled } from '@tabler/icons-react';
import React from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(programPenghijauanState); const state = useProxy(programPenghijauanState);
@@ -17,7 +16,7 @@ function Page() {
const { data, load, page, totalPages, loading } = state.findMany; const { data, load, page, totalPages, loading } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, debouncedSearch); load(page, 4, debouncedSearch);
}, [page, debouncedSearch]); }, [page, debouncedSearch]);
const iconMap: Record<string, any> = { const iconMap: Record<string, any> = {
@@ -110,9 +109,6 @@ function Page() {
<Text fz="sm" ta="center" c="dimmed"> <Text fz="sm" ta="center" c="dimmed">
{v.judul} {v.judul}
</Text> </Text>
<Button variant="gradient" gradient={{ from: colors['blue-button'], to: 'cyan' }} size="sm" radius="xl" mt="sm">
Pelajari Lebih Lanjut
</Button>
</Stack> </Stack>
</Paper> </Paper>
))} ))}

View File

@@ -42,7 +42,7 @@ function Page() {
Program Bimbingan Belajar Desa Program Bimbingan Belajar Desa
</Title> </Title>
<Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} /> <Divider size="sm" my="md" mx="auto" w="60%" color={colors['blue-button']} />
<Text ta="center" fz="lg" c="dimmed" px={{ base: 'sm', md: 120 }}> <Text ta="center" fz="lg" c="black" px={{ base: 'sm', md: 120 }}>
Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini. Program unggulan untuk mendukung siswa Desa Darmasaba memahami pelajaran sekolah, meningkatkan prestasi akademik, dan menumbuhkan semangat belajar sejak dini.
</Text> </Text>
</Box> </Box>

View File

@@ -239,9 +239,9 @@ export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan:
paddingRight: 20, paddingRight: 20,
}} }}
onClick={() => { onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/lembaga`); if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/siswa`); if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/pengajar`); if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/${jenjangPendidikan}/pengajar`);
}} }}
> >
Lihat Detail Lihat Detail

View File

@@ -74,8 +74,8 @@ function Page({ params }: PageProps) {
> >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="50%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="50%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>

View File

@@ -75,7 +75,7 @@ function Page({ params }: PageProps) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="30%">Nama Pengajar</TableTh> <TableTh w="30%">Nama Pengajar</TableTh>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>

View File

@@ -75,7 +75,7 @@ function Page({ params }: PageProps) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh w="30%">Nama Siswa</TableTh> <TableTh w="30%">Nama Siswa</TableTh>
<TableTh w="60%">Nama Lembaga</TableTh> <TableTh w="30%">Nama Lembaga</TableTh>
<TableTh w="40%">Jenjang Pendidikan</TableTh> <TableTh w="40%">Jenjang Pendidikan</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>

View File

@@ -67,10 +67,10 @@ export default function LayoutSekolah({
// arahkan langsung ke route jenjang pendidikan // arahkan langsung ke route jenjang pendidikan
if (k.toLowerCase() === 'semua') { if (k.toLowerCase() === 'semua') {
setJenjangPendidikanAktif(k); setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua`); router.push(`/darmasaba/pendidikan/info-sekolah/semua`);
} else { } else {
setJenjangPendidikanAktif(k); setJenjangPendidikanAktif(k);
router.push(`/darmasaba/pendidikan/info-sekolah-paud/${encodeURIComponent(k.toLowerCase())}`); router.push(`/darmasaba/pendidikan/info-sekolah/${encodeURIComponent(k.toLowerCase())}`);
} }
}; };

View File

@@ -250,9 +250,9 @@ export default function SekolahPage() {
paddingRight: 20, paddingRight: 20,
}} }}
onClick={() => { onClick={() => {
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/lembaga`); if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah/semua/lembaga`);
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/siswa`); if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/siswa`);
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/pengajar`); if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah/semua/pengajar`);
}} }}
> >
Lihat Detail Lihat Detail

View File

@@ -1,35 +1,34 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Button, Center, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput } from '@mantine/core'; import { ActionIcon, Box, Button, Center, Flex, Group, Image, Paper, SimpleGrid, Skeleton, Spoiler, Stack, Text, TextInput } from '@mantine/core';
import { IconSearch, IconUser } from '@tabler/icons-react'; import { IconSearch, IconUser } from '@tabler/icons-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
import Link from 'next/link'; import Link from 'next/link';
import { useProxy } from 'valtio/utils';
import perpustakaanDigitalState from '@/app/admin/(dashboard)/_state/pendidikan/perpustakaan-digital';
const dataSekolah = [ import { useShallowEffect } from '@mantine/hooks';
{ import { useState } from 'react';
id: 1,
gambar: '/api/img/buku-1.png',
judul: 'Angkasa dan 56 Hari',
deskripsi: 'Angkasa dan 56 hari mengisahkan tentang sebuah perjuangan perihal asa yang belum usai. Tentang cinta pertama yang secara tiba-tiba menghilang dari kehidupan.'
},
{
id: 2,
gambar: '/api/img/buku-2.png',
judul: 'Sayuran Organik',
deskripsi: 'Buku ini membahas cara menanam sayuran secara organik, jenis-jenis sayuran organik, dan cara mengatasi hama dan penyakit. '
},
{
id: 3,
gambar: '/api/img/buku-3.png',
judul: 'Bali Tempo Dulu',
deskripsi: 'Buku Bali Tempo Doeloe oleh Adrian Vickers berisi berbagai catatan perjalanan yang menggambarkan kehidupan sosial budaya Bali di masa lampau.'
},
]
function Page() { function Page() {
const state = useProxy(perpustakaanDigitalState)
const [expandedId, setExpandedId] = useState<string | null>(null);
useShallowEffect(() => {
state.dataPerpustakaan.findMany.load()
}, [])
if (!state.dataPerpustakaan.findMany.load)
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Skeleton h={60} radius="xl" />
<Skeleton h={200} mt="lg" radius="md" />
</Box>
</Stack>
)
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
@@ -80,22 +79,54 @@ function Page() {
base: 1, base: 1,
md: 3 md: 3
}} }}
style={{
alignItems: 'stretch'
}}
> >
{dataSekolah.map((v, k) => { {state.dataPerpustakaan.findMany.data.map((v, k) => {
return ( return (
<Box key={k}> <Box key={k} style={{ height: '100%' }}>
<motion.div <motion.div
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.8 }}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
> >
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}> <Paper
<Stack> p={"xl"}
bg={colors['white-trans-1']}
w={{ base: "100%", md: "100%" }}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column'
}}
>
<Stack style={{ flex: 1 }}>
<Center> <Center>
<Image src={v.gambar} alt='' w={{ base: 390, md: 1000 }}/> <Image src={v.image.link} alt='' w={{ base: 390, md: 1000 }} />
</Center> </Center>
<Text c={colors["blue-button"]} ta={'center'} fw={'bold'} fz={{ base: "h2", md: "h1" }}>{v.judul}</Text> <Text c={colors["blue-button"]} ta={'center'} fw={'bold'} fz={{ base: "h2", md: "h1" }}>{v.judul}</Text>
<Text c={colors["blue-button"]} ta={'center'} fw={'bold'}>{v.deskripsi}</Text> <Spoiler
showLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Show more
</Text>
}
hideLabel={
<Text fw="bold" fz="sm" c={colors['blue-button']}>
Hide details
</Text>
}
expanded={expandedId === v.id}
onExpandedChange={(isExpanded) =>
setExpandedId(isExpanded ? v.id : null)
}
>
<Text
ta="justify"
fz="sm"
dangerouslySetInnerHTML={{ __html: v.deskripsi }}
/>
</Spoiler>
</Stack> </Stack>
</Paper> </Paper>
</motion.div> </motion.div>

View File

@@ -302,12 +302,12 @@ const navbarListMenu = [
}, { }, {
id: "8", id: "8",
name: "Pendidikan", name: "Pendidikan",
href: "/darmasaba/pendidikan/info-sekolah-paud", href: "/darmasaba/pendidikan/info-sekolah",
children: [ children: [
{ {
id: "8.1", id: "8.1",
name: "Info Sekolah & PAUD", name: "Info Sekolah",
href: "/darmasaba/pendidikan/info-sekolah-paud/semua" href: "/darmasaba/pendidikan/info-sekolah/semua"
}, },
{ {
id: "8.2", id: "8.2",