fix tampilan admin menu inovasi, sisa menu lingkungan

This commit is contained in:
2025-09-22 10:53:48 +08:00
parent 8e25c91e85
commit 0fc47c28ff
52 changed files with 3787 additions and 1723 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -61,10 +62,37 @@ const ajukanIdeInovatifState = proxy({
}; };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.inovasi.ajukanideinovatif["find-many"].get(); totalPages: 1,
if (res.status === 200) { loading: false,
ajukanIdeInovatifState.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
ajukanIdeInovatifState.findMany.loading = true; // ✅ Akses langsung via nama path
ajukanIdeInovatifState.findMany.page = page;
ajukanIdeInovatifState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.ajukanideinovatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
ajukanIdeInovatifState.findMany.data = res.data.data ?? [];
ajukanIdeInovatifState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch ajukan ide inovatif paginated:", err);
ajukanIdeInovatifState.findMany.data = [];
ajukanIdeInovatifState.findMany.totalPages = 1;
} finally {
ajukanIdeInovatifState.findMany.loading = false;
} }
}, },
}, },
@@ -97,16 +125,21 @@ const ajukanIdeInovatifState = proxy({
try { try {
ajukanIdeInovatifState.delete.loading = true; ajukanIdeInovatifState.delete.loading = true;
const response = await fetch(`/api/inovasi/ajukanideinovatif/del/${id}`, { const response = await fetch(
method: "DELETE", `/api/inovasi/ajukanideinovatif/del/${id}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok) { if (response.ok) {
toast.success(result.message || "Ajukan Ide Inovatif berhasil dihapus"); toast.success(
result.message || "Ajukan Ide Inovatif berhasil dihapus"
);
await ajukanIdeInovatifState.findMany.load(); await ajukanIdeInovatifState.findMany.load();
} else { } else {
toast.error(result?.message || "Gagal menghapus ajukan ide inovatif"); toast.error(result?.message || "Gagal menghapus ajukan ide inovatif");

View File

@@ -64,8 +64,8 @@ const administrasiOnline = proxy({
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: '', search: "",
async load(page = 1, limit = 10, search = '') { async load(page = 1, limit = 10, search = "") {
administrasiOnline.findMany.loading = true; administrasiOnline.findMany.loading = true;
administrasiOnline.findMany.page = page; administrasiOnline.findMany.page = page;
administrasiOnline.findMany.search = search; administrasiOnline.findMany.search = search;
@@ -215,14 +215,14 @@ const jenisLayanan = proxy({
const query: any = { page, limit }; const query: any = { page, limit };
if (search) query.search = search; if (search) query.search = search;
const res = await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[ const res =
"find-many" await ApiFetch.api.inovasi.layananonlinedesa.administrasionline.jenislayanan[
].get({ query }); "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
jenisLayanan.findMany.data = res.data.data ?? []; jenisLayanan.findMany.data = res.data.data ?? [];
jenisLayanan.findMany.totalPages = jenisLayanan.findMany.totalPages = res.data.totalPages ?? 1;
res.data.totalPages ?? 1;
} else { } else {
jenisLayanan.findMany.data = []; jenisLayanan.findMany.data = [];
jenisLayanan.findMany.totalPages = 1; jenisLayanan.findMany.totalPages = 1;
@@ -494,27 +494,32 @@ const pengaduanMasyarakat = proxy({
page: 1, page: 1,
totalPages: 1, totalPages: 1,
loading: false, loading: false,
search: "",
async load(page = 1, limit = 10) { load: async (page = 1, limit = 10, search = "") => {
pengaduanMasyarakat.findMany.loading = true; pengaduanMasyarakat.findMany.loading = true; // ✅ Akses langsung via nama path
pengaduanMasyarakat.findMany.page = page; pengaduanMasyarakat.findMany.page = page;
pengaduanMasyarakat.findMany.search = search;
try { try {
const query: any = { page, limit };
if (search) query.search = search;
const res = const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[ await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat[
"find-many" "find-many"
].get({ ].get({ query });
query: {
page,
limit,
},
});
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
pengaduanMasyarakat.findMany.data = res.data.data ?? []; pengaduanMasyarakat.findMany.data = res.data.data ?? [];
pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1; pengaduanMasyarakat.findMany.totalPages = res.data.totalPages ?? 1;
} else {
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} }
} catch (err) { } catch (err) {
console.error("Gagal fetch pengaduan masyarakat paginated:", err); console.error("Gagal fetch pengaduan masyarakat paginated:", err);
pengaduanMasyarakat.findMany.data = [];
pengaduanMasyarakat.findMany.totalPages = 1;
} finally { } finally {
pengaduanMasyarakat.findMany.loading = false; pengaduanMasyarakat.findMany.loading = false;
} }
@@ -634,13 +639,37 @@ const jenisPengaduan = proxy({
id: string; id: string;
nama: string; nama: string;
}> | null, }> | null,
async load() { page: 1,
const res = totalPages: 1,
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jenisPengaduan.findMany.loading = true; // ✅ Akses langsung via nama path
jenisPengaduan.findMany.data = res.data?.data ?? []; jenisPengaduan.findMany.page = page;
jenisPengaduan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res =
await ApiFetch.api.inovasi.layananonlinedesa.pengaduanmasyarakat.jenispengaduan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jenisPengaduan.findMany.data = res.data.data ?? [];
jenisPengaduan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jenis pengaduan paginated:", err);
jenisPengaduan.findMany.data = [];
jenisPengaduan.findMany.totalPages = 1;
} finally {
jenisPengaduan.findMany.loading = false;
} }
}, },
}, },

View File

@@ -54,34 +54,32 @@ const programKreatifState = proxy({
data: null as any[] | null, data: null as any[] | null,
page: 1, page: 1,
totalPages: 1, totalPages: 1,
total: 0,
loading: false, loading: false,
load: async (page = 1, limit = 10) => { search: "",
// Change to arrow function load: async (page = 1, limit = 10, search = "") => {
programKreatifState.findMany.loading = true; // Use the full path to access the property programKreatifState.findMany.loading = true; // ✅ Akses langsung via nama path
programKreatifState.findMany.page = page; programKreatifState.findMany.page = page;
programKreatifState.findMany.search = search;
try { try {
const res = await ApiFetch.api.inovasi.programkreatif["find-many"].get({ const query: any = { page, limit };
query: { page, limit }, if (search) query.search = search;
});
const res = await ApiFetch.api.inovasi.programkreatif[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) { if (res.status === 200 && res.data?.success) {
programKreatifState.findMany.data = res.data.data || []; programKreatifState.findMany.data = res.data.data ?? [];
programKreatifState.findMany.total = res.data.total || 0; programKreatifState.findMany.totalPages =
programKreatifState.findMany.totalPages = res.data.totalPages || 1; res.data.totalPages ?? 1;
} else { } else {
console.error(
"Failed to load grafik berdasarkan jenis kelamin:",
res.data?.message
);
programKreatifState.findMany.data = []; programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1; programKreatifState.findMany.totalPages = 1;
} }
} catch (error) { } catch (err) {
console.error("Error loading grafik berdasarkan jenis kelamin:", error); console.error("Gagal fetch program kreatif paginated:", err);
programKreatifState.findMany.data = []; programKreatifState.findMany.data = [];
programKreatifState.findMany.total = 0;
programKreatifState.findMany.totalPages = 1; programKreatifState.findMany.totalPages = 1;
} finally { } finally {
programKreatifState.findMany.loading = false; programKreatifState.findMany.loading = false;

View File

@@ -0,0 +1,290 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateForm = z.object({
name: z.string().min(1, "Name minimal 1 karakter"),
tanggal: z.string().min(1, "Tanggal minimal 1 karakter"),
namaOrangtua: z.string().min(1, "Nama Orangtua minimal 1 karakter"),
nomor: z.string().min(1, "Nomor minimal 1 karakter"),
alamat: z.string().min(1, "Alamat minimal 1 karakter"),
catatan: z.string().min(1, "Catatan minimal 1 karakter"),
});
const defaultForm = {
name: "",
tanggal: "",
namaOrangtua: "",
nomor: "",
alamat: "",
catatan: "",
};
const pendaftaranJadwalKegiatanState = proxy({
create: {
form: { ...defaultForm },
loading: false,
async submit() {
const cek = templateForm.safeParse(this.form);
if (!cek.success) {
const errMsg = cek.error.issues
.map((v) => `${v.path.join(".")}: ${v.message}`)
.join("\n");
toast.error(errMsg);
return null;
}
try {
this.loading = true;
const payload = { ...this.form };
const res = await (ApiFetch.api.kesehatan as any)[
"pendaftaran-jadwal-kegiatan"
].create.post(payload);
if (res.status === 200) {
toast.success("Berhasil menambahkan jadwal kegiatan");
this.resetForm();
await pendaftaranJadwalKegiatanState.findMany.load();
return res.data;
}
} catch (err: any) {
const msg = err?.message || "Terjadi kesalahan saat mengirim data";
toast.error(msg);
console.error("SUBMIT ERROR:", err);
return null;
} finally {
this.loading = false;
}
},
resetForm() {
this.form = { ...defaultForm };
},
},
findMany: {
data: null as
| Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
pendaftaranJadwalKegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
pendaftaranJadwalKegiatanState.findMany.page = page;
pendaftaranJadwalKegiatanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["pendaftaran-jadwal-kegiatan"][
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
pendaftaranJadwalKegiatanState.findMany.data = res.data.data ?? [];
pendaftaranJadwalKegiatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
}
} catch (err) {
console.error(
"Gagal fetch pendaftaran jadwal kegiatan paginated:",
err
);
pendaftaranJadwalKegiatanState.findMany.data = [];
pendaftaranJadwalKegiatanState.findMany.totalPages = 1;
} finally {
pendaftaranJadwalKegiatanState.findMany.loading = false;
}
},
},
findUnique: {
data: null as Prisma.PendaftaranJadwalKegiatanGetPayload<{
omit: {
isActive: true;
};
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`
);
if (res.ok) {
const data = await res.json();
pendaftaranJadwalKegiatanState.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
pendaftaranJadwalKegiatanState.findUnique.data = null;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
pendaftaranJadwalKegiatanState.delete.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Pendaftaran jadwal kegiatan berhasil dihapus"
);
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus pendaftaran jadwal kegiatan"
);
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error(
"Terjadi kesalahan saat menghapus pendaftaran jadwal kegiatan"
);
} finally {
pendaftaranJadwalKegiatanState.delete.loading = false;
}
},
},
edit: {
id: "",
form: { ...defaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/kesehatan/pendaftaran-jadwal-kegiatan/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
name: data.name,
tanggal: data.tanggal,
namaOrangtua: data.namaOrangtua,
nomor: data.nomor,
alamat: data.alamat,
catatan: data.catatan,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateForm.safeParse(pendaftaranJadwalKegiatanState.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pendaftaranJadwalKegiatanState.edit.loading = true;
const response = await fetch(
`/api/kesehatan/pendaftaran-jadwal-kegiatan/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: this.form.name,
tanggal: this.form.tanggal,
namaOrangtua: this.form.namaOrangtua,
nomor: this.form.nomor,
alamat: this.form.alamat,
catatan: this.form.catatan,
}),
}
);
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 pendaftaran jadwal kegiatan");
await pendaftaranJadwalKegiatanState.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pendaftaran jadwal kegiatan");
}
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update pendaftaran jadwal kegiatan"
);
return false;
} finally {
pendaftaranJadwalKegiatanState.edit.loading = false;
}
},
reset() {
pendaftaranJadwalKegiatanState.edit.id = "";
pendaftaranJadwalKegiatanState.edit.form = { ...defaultForm };
},
},
});
export default pendaftaranJadwalKegiatanState;

View File

@@ -1,8 +1,8 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -10,87 +10,114 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif'; import ajukanIdeInovatifState from '../../../_state/inovasi/ajukan-ide-inovatif';
function DetailAjukanIdeInofativDesa() { function DetailAjukanIdeInofativDesa() {
const state = useProxy(ajukanIdeInovatifState) const state = useProxy(ajukanIdeInovatifState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
state.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
state.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/inovasi/ajukan-ide-inovatif") router.push("/admin/inovasi/ajukan-ide-inovatif");
} }
} };
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Flex justify="space-between" gap={"xs"}> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Ajukan Ide Inovatif Desa</Text> </Button>
<Button
onClick={() => { {/* Card Utama */}
if (state.findUnique.data) { <Paper
setSelectedId(state.findUnique.data.id); withBorder
w={{ base: "100%", md: "80%", lg: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
{/* Header */}
<Flex justify="space-between" align="center">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Ajukan Ide Inovatif Desa
</Text>
<Tooltip label="Hapus Ide Inovatif" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true); setModalHapus(true);
} }}
}} variant="light"
disabled={state.delete.loading || !state.findUnique.data} radius="md"
color={"red"} size="md"
> disabled={state.delete.loading}
<IconX size={20} /> >
</Button> <IconTrash size={20} />
</Button>
</Tooltip>
</Flex> </Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> {/* Detail Data */}
<Stack gap={"xs"}> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Nama</Text> <Box>
<Text fz={"lg"}>{state.findUnique.data?.name}</Text> <Text fz="lg" fw="bold">Nama</Text>
</Box> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
<Box> </Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.alamat }} /> <Box>
</Box> <Text fz="lg" fw="bold">Alamat</Text>
<Box> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.alamat || '-' }} />
<Text fw={"bold"} fz={"lg"}>Nama Ide Inovatif</Text> </Box>
<Text fz={"lg"}>{state.findUnique.data?.namaIde}</Text>
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Nama Ide Inovatif</Text>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> <Text fz="md" c="dimmed">{data?.namaIde || '-'}</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }} /> </Box>
</Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Masalah</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"}>{state.findUnique.data?.masalah}</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Benefit</Text> <Box>
<Text fz={"lg"}>{state.findUnique.data?.benefit}</Text> <Text fz="lg" fw="bold">Masalah</Text>
</Box> <Text fz="md" c="dimmed">{data?.masalah || '-'}</Text>
</Stack> </Box>
</Paper>
) : null} <Box>
<Text fz="lg" fw="bold">Benefit</Text>
<Text fz="md" c="dimmed">{data?.benefit || '-'}</Text>
</Box>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -99,7 +126,7 @@ function DetailAjukanIdeInofativDesa() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus ajukan ide inovatif ini?' text="Apakah anda yakin ingin menghapus ajukan ide inovatif ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,8 +1,26 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box,
Button,
Center,
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 { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch, IconPlus } 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';
@@ -15,7 +33,7 @@ function AjukanIdeInovatif() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Ajukan Ide Inovatif' title='Ajukan Ide Inovatif'
placeholder='pencarian' placeholder='Cari ide inovatif...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,62 +46,110 @@ function AjukanIdeInovatif() {
function ListAjukanIdeInovatif({ search }: { search: string }) { function ListAjukanIdeInovatif({ search }: { search: string }) {
const state = useProxy(ajukanIdeInovatifState) const state = useProxy(ajukanIdeInovatifState)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
state.findMany.load() load(page, 10, search)
}, []) }, [page, search])
const filteredData = (state.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword) ||
item.namaIde.toLowerCase().includes(keyword) ||
item.masalah.toLowerCase().includes(keyword) ||
item.benefit.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title mb={10} order={3}>List Ajukan Ide Inovatif</Title> <Group justify="space-between" mb="md">
<Table striped withTableBorder withRowBorders> <Title order={4}>Daftar Ide Inovatif</Title>
<TableThead> <Tooltip label="Ajukan Ide Baru" withArrow>
<TableTr> <Button
<TableTh>Nama</TableTh> leftSection={<IconPlus size={18} />}
<TableTh>Alamat</TableTh> color="blue"
<TableTh>Nama Ide Inovatif</TableTh> variant="light"
<TableTh>Detail</TableTh> onClick={() => router.push('/admin/inovasi/ajukan-ide-inovatif/create')}
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item) => ( </Tooltip>
<TableTr key={item.id}> </Group>
<TableTd>{item.name}</TableTd>
<TableTd> <Box style={{ overflowX: "auto" }}>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.alamat }} /> <Table highlightOnHover>
</TableTd> <TableThead>
<TableTd> <TableTr>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.namaIde }} /> <TableTh style={{ width: '20%' }}>Nama</TableTh>
</TableTd> <TableTh style={{ width: '30%' }}>Alamat</TableTh>
<TableTd> <TableTh style={{ width: '30%' }}>Nama Ide Inovatif</TableTh>
<Button onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
<IconDeviceImac size={20} /> </TableTr>
</Button> </TableThead>
</TableTd> <TableTbody>
</TableTr> {filteredData.length > 0 ? (
))} filteredData.map((item) => (
</TableTbody> <TableTr key={item.id}>
</Table> <TableTd>
<Text fw={500} truncate lineClamp={1}>{item.name}</Text>
</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.alamat }} />
</TableTd>
<TableTd>
<Text truncate fz="sm" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.namaIde }} />
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/ajukan-ide-inovatif/${item.id}`)}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada ide inovatif yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno'; import infoTeknoState from '@/app/admin/(dashboard)/_state/inovasi/info-tekno';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -13,22 +24,23 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditInfoTeknologiTepatGuna() { function EditInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState) const stateInfoTekno = useProxy(infoTeknoState);
const router = useRouter() const router = useRouter();
const params = useParams() const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null)
const [file, setFile] = useState<File | null>(null) const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: stateInfoTekno.findUnique.data?.name || '', name: stateInfoTekno.findUnique.data?.name || '',
deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '', deskripsi: stateInfoTekno.findUnique.data?.deskripsi || '',
imageId: stateInfoTekno.findUnique.data?.imageId || '', imageId: stateInfoTekno.findUnique.data?.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const loadPenghargaan = async () => { const id = params?.id as string;
const id = params?.id as string; if (!id) return;
if (!id) return;
const loadPenghargaan = async () => {
try { try {
const data = await stateInfoTekno.edit.load(id); const data = await stateInfoTekno.edit.load(id);
if (data) { if (data) {
@@ -38,13 +50,11 @@ function EditInfoTeknologiTepatGuna() {
imageId: data.imageId || '', imageId: data.imageId || '',
}); });
if (data?.image?.link) { if (data?.image?.link) setPreviewImage(data.image.link);
setPreviewImage(data.image.link);
}
} }
} catch (error) { } catch (error) {
console.error("Error loading info teknologi tepat guna:", error); console.error('Error loading info teknologi tepat guna:', error);
toast.error("Gagal memuat data info teknologi tepat guna"); toast.error('Gagal memuat data info teknologi tepat guna');
} }
}; };
@@ -55,104 +65,127 @@ function EditInfoTeknologiTepatGuna() {
try { try {
stateInfoTekno.edit.form = { stateInfoTekno.edit.form = {
...stateInfoTekno.edit.form, ...stateInfoTekno.edit.form,
name: formData.name, ...formData,
deskripsi: formData.deskripsi, };
imageId: formData.imageId,
}
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal upload gambar');
} }
stateInfoTekno.edit.form.imageId = uploaded.id; stateInfoTekno.edit.form.imageId = uploaded.id;
} }
await stateInfoTekno.edit.update(); await stateInfoTekno.edit.update();
toast.success("Info teknologi tepat guna berhasil diperbarui!"); toast.success('Info teknologi tepat guna berhasil diperbarui!');
router.push("/admin/inovasi/info-teknologi-tepat-guna"); router.push('/admin/inovasi/info-teknologi-tepat-guna');
} catch (error) { } catch (error) {
console.error("Error updating info teknologi tepat guna:", error); console.error('Error updating info teknologi tepat guna:', error);
toast.error("Terjadi kesalahan saat memperbarui info teknologi tepat guna"); toast.error('Terjadi kesalahan saat memperbarui info teknologi tepat guna');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Tombol back + title */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors["blue-button"]} size={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Edit Info Teknologi Tepat Guna</Title> <Title order={4} ml="sm" c="dark">
Edit Info Teknologi Tepat Guna
</Title>
</Group>
{/* Card form */}
<Paper
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Judul */}
<TextInput <TextInput
label="Judul"
placeholder="Masukkan judul info teknologi tepat guna"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Judul</Text>} required
placeholder="masukkan judul"
/> />
{/* Upload gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar wajib
</Text>
</Stack>
</Group>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{
</div> maxHeight: 220,
</Group> objectFit: 'contain',
</Dropzone> border: `1px solid ${colors['blue-button']}`,
}}
{/* Tampilkan preview kalau ada */} loading="lazy"
{previewImage && ( />
<Box mt="sm"> </Box>
<Image )}
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box> </Box>
{/* Deskripsi pakai editor */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -162,7 +195,21 @@ function EditInfoTeknologiTepatGuna() {
/> />
</Box> </Box>
<Button onClick={handleSubmit}>Simpan</Button> {/* Tombol submit */}
<Group justify="right">
<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 colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -32,64 +32,106 @@ function DetailInfoTeknologiTepatGuna() {
if (!stateInfoTekno.findUnique.data) { if (!stateInfoTekno.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
const data = stateInfoTekno.findUnique.data
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Info Teknologi Tepat Guna</Text> Kembali
{stateInfoTekno.findUnique.data ? ( </Button>
<Paper key={stateInfoTekno.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Card Utama */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Judul</Text> withBorder
<Text fz={"lg"}>{stateInfoTekno.findUnique.data?.name}</Text> w={{ base: "100%", md: "70%", lg: "60%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> radius="md"
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateInfoTekno.findUnique.data?.deskripsi }} /> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fw={"bold"} fz={"lg"}>Gambar</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Image w={{ base: 150, md: 150, lg: 150 }} src={stateInfoTekno.findUnique.data?.image?.link} alt="gambar" loading="lazy"/> Detail Info Teknologi Tepat Guna
</Box> </Text>
<Flex gap={"xs"} mt={10}>
<Paper bg={colors['BG-trans']} p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Judul</Text>
<Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Teknologi'}
w={150}
h={150}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Buttons */}
<Group gap="sm" mt={10}>
<Tooltip label="Hapus Info Teknologi" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateInfoTekno.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateInfoTekno.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={stateInfoTekno.delete.loading || !stateInfoTekno.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={stateInfoTekno.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Info Teknologi" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateInfoTekno.findUnique.data) { onClick={() =>
router.push(`/admin/inovasi/info-teknologi-tepat-guna/${stateInfoTekno.findUnique.data.id}/edit`); router.push(`/admin/inovasi/info-teknologi-tepat-guna/${data.id}/edit`)
} }
}} variant="light"
disabled={!stateInfoTekno.findUnique.data} radius="md"
color={"green"} size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,7 +1,18 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -11,76 +22,92 @@ import CreateEditor from '../../../_com/createEditor';
import infoTeknoState from '../../../_state/inovasi/info-tekno'; import infoTeknoState from '../../../_state/inovasi/info-tekno';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
function CreateInfoTeknologiTepatGuna() { function CreateInfoTeknologiTepatGuna() {
const stateInfoTekno = useProxy(infoTeknoState) const stateInfoTekno = useProxy(infoTeknoState);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateInfoTekno.create.form = { stateInfoTekno.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
} };
setPreviewImage(null) setPreviewImage(null);
setFile(null) setFile(null);
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.error("Silahkan pilih file gambar terlebih dahulu") return toast.error('Silahkan pilih file gambar terlebih dahulu');
} }
try { try {
// Upload the image first
const uploadRes = await ApiFetch.api.fileStorage.create.post({ const uploadRes = await ApiFetch.api.fileStorage.create.post({
file: file, file: file,
name: file.name name: file.name,
}) });
const uploaded = uploadRes.data?.data const uploaded = uploadRes.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar") return toast.error('Gagal upload gambar');
} }
// Set the image ID in the form stateInfoTekno.create.form.imageId = uploaded.id;
stateInfoTekno.create.form.imageId = uploaded.id
// Submit the form const success = await stateInfoTekno.create.create();
const success = await stateInfoTekno.create.create()
if (success) { if (success) {
resetForm() resetForm();
router.push("/admin/inovasi/info-teknologi-tepat-guna") router.push('/admin/inovasi/info-teknologi-tepat-guna');
} }
} catch (error) { } catch (error) {
console.error("Error in handleSubmit:", error) console.error('Error in handleSubmit:', error);
toast.error("Terjadi kesalahan saat menyimpan data") toast.error('Terjadi kesalahan saat menyimpan data');
} }
};
}
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}> </Button>
<Stack gap={"xs"}> </Tooltip>
<Title order={3}>Create Info Teknologi Tepat Guna</Title> <Title order={4} ml="sm" c="dark">
Tambah Info Teknologi Tepat Guna
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Nama */}
<TextInput <TextInput
value={stateInfoTekno.create.form.name} value={stateInfoTekno.create.form.name}
onChange={(val) => { onChange={(val) => {
stateInfoTekno.create.form.name = val.target.value; stateInfoTekno.create.form.name = val.target.value;
}} }}
label={<Text fz={"sm"} fw={"bold"}>Nama Info Teknologi Tepat Guna</Text>} label="Nama Info Teknologi Tepat Guna"
placeholder="masukkan nama info teknologi tepat guna" placeholder="Masukkan nama info teknologi tepat guna"
required
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor <CreateEditor
value={stateInfoTekno.create.form.deskripsi} value={stateInfoTekno.create.form.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -88,63 +115,70 @@ function CreateInfoTeknologiTepatGuna() {
}} }}
/> />
</Box> </Box>
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid.')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" c="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> loading="lazy"
</Group> />
</Dropzone> </Box>
)}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Simpan</Button>
{/* Submit Button */}
<Group justify="right">
<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,13 +1,30 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
Box,
Button,
Center,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Group,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } 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 infoTeknoState from '../../_state/inovasi/info-tekno'; import infoTeknoState from '../../_state/inovasi/info-tekno';
function InfoTeknologiTepatGuna() { function InfoTeknologiTepatGuna() {
@@ -15,8 +32,8 @@ function InfoTeknologiTepatGuna() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Info Teknologi Tepat Guna' title="Info Teknologi Tepat Guna"
placeholder='pencarian' placeholder="Cari info teknologi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,70 +44,125 @@ function InfoTeknologiTepatGuna() {
} }
function ListInfoTeknologiTepatGuna({ search }: { search: string }) { function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
const state = useProxy(infoTeknoState) const state = useProxy(infoTeknoState);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = state.findMany;
data,
page,
totalPages,
loading,
load,
} = state.findMany
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !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 withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Info Teknologi Tepat Guna' <Title order={4}>Daftar Info Teknologi Tepat Guna</Title>
href='/admin/inovasi/info-teknologi-tepat-guna/create' <Tooltip label="Tambah Info Teknologi" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Info Teknologi Tepat Guna</TableTh> onClick={() =>
<TableTh>Deskripsi Singkat Info Teknologi Tepat Guna</TableTh> router.push('/admin/inovasi/info-teknologi-tepat-guna/create')
<TableTh>Detail</TableTh> }
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item) => ( </Tooltip>
<TableTr key={item.id}> </Group>
<TableTd>{item.name}</TableTd>
<TableTd> <Box style={{ overflowX: 'auto' }}>
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <Table highlightOnHover>
</TableTd> <TableThead>
<TableTd> <TableTr>
<Button onClick={() => router.push(`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`)}> <TableTh style={{ width: '30%' }}>
<IconDeviceImac size={20} /> Nama Info Teknologi
</Button> </TableTh>
</TableTd> <TableTh style={{ width: '50%' }}>
Deskripsi Singkat
</TableTh>
<TableTh style={{ width: '20%' }}>Aksi</TableTh>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
<Center> filteredData.map((item) => (
<Pagination <TableTr key={item.id}>
value={page} <TableTd style={{ width: '30%' }}>
onChange={(newPage) => load(newPage)} // ini penting! <Text fw={500} truncate="end" lineClamp={1}>
total={totalPages} {item.name}
my="md" </Text>
/> </TableTd>
</Center> <TableTd style={{ width: '50%' }}>
<Text
lineClamp={1}
truncate
fz="sm"
c="dimmed"
dangerouslySetInnerHTML={{
__html: item.deskripsi || '-',
}}
/>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/info-teknologi-tepat-guna/${item.id}`,
)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data Info Teknologi yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
); );
} }

View File

@@ -1,62 +1,118 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import { ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } 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 { IconListDetails, IconUsers } from '@tabler/icons-react';
function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) { function LayoutTabsKolaborasi({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "List Kolaborasi Inovasi",
value: "listkolaborasiinovasi",
href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi"
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi"
}
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "List Kolaborasi Inovasi",
router.push(tab.href) value: "listkolaborasiinovasi",
} href: "/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi",
setActiveTab(value) tooltip: "Lihat daftar kolaborasi inovasi",
icon: <IconListDetails size={18} stroke={1.8} />,
},
{
label: "Mitra Kolaborasi",
value: "mitarakolaborasi",
href: "/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi",
tooltip: "Kelola mitra kolaborasi",
icon: <IconUsers size={18} stroke={1.8} />,
} }
];
useEffect(() => { const currentTab = tabs.find(tab => tab.href === pathname);
const match = tabs.find(tab => tab.href === pathname) const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return ( const handleTabChange = (value: string | null) => {
<Stack> const tab = tabs.find(t => t.value === value);
<Title order={3}>Kolaborasi Inovasi</Title> if (tab) {
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> router.push(tab.href);
<TabsList p={"xs"} bg={"#BBC8E7FF"}> }
{tabs.map((e, i) => ( setActiveTab(value);
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> };
))}
</TabsList> useEffect(() => {
{tabs.map((e, i) => ( const match = tabs.find(tab => tab.href === pathname);
<TabsPanel key={i} value={e.value}> if (match) {
{/* Konten dummy, bisa diganti tergantung routing */} setActiveTab(match.value);
<></> }
</TabsPanel> }, [pathname]);
))}
</Tabs> return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Kolaborasi Inovasi
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper biar rapi */}
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: 'pop', duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children} {children}
</Stack> </TabsPanel>
); ))}
</Tabs>
</Stack>
);
} }
export default LayoutTabsKolaborasi; export default LayoutTabsKolaborasi;

View File

@@ -7,11 +7,13 @@ import colors from "@/con/colors";
import { import {
Box, Box,
Button, Button,
Group,
Paper, Paper,
Stack, Stack,
Text, Text,
TextInput, TextInput,
Title Title,
Tooltip,
} from "@mantine/core"; } 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";
@@ -25,33 +27,33 @@ function EditKolaborasiInovasi() {
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: kolaborasiState.update.form.name || '', name: kolaborasiState.update.form.name || "",
deskripsi: kolaborasiState.update.form.deskripsi || '', deskripsi: kolaborasiState.update.form.deskripsi || "",
tahun: kolaborasiState.update.form.tahun || '', tahun: kolaborasiState.update.form.tahun || "",
slug: kolaborasiState.update.form.slug || '', slug: kolaborasiState.update.form.slug || "",
kolaborator: kolaborasiState.update.form.kolaborator || '', kolaborator: kolaborasiState.update.form.kolaborator || "",
}); });
// Load berita by id saat pertama kali // Load data
useEffect(() => { useEffect(() => {
const loadKolaborasi = async () => { const loadKolaborasi = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await kolaborasiState.update.load(id); // akses langsung, bukan dari proxy const data = await kolaborasiState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || "",
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || "",
tahun: data.tahun || '', tahun: data.tahun || "",
slug: data.slug || '', slug: data.slug || "",
kolaborator: data.kolaborator || '', kolaborator: data.kolaborator || "",
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading berita:", error); console.error("Error loading kolaborasi:", error);
toast.error("Gagal memuat data berita"); toast.error("Gagal memuat data kolaborasi inovasi");
} }
}; };
@@ -59,9 +61,7 @@ function EditKolaborasiInovasi() {
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Update global state with form data
kolaborasiState.update.form = { kolaborasiState.update.form = {
...kolaborasiState.update.form, ...kolaborasiState.update.form,
name: formData.name, name: formData.name,
@@ -71,53 +71,72 @@ function EditKolaborasiInovasi() {
kolaborator: formData.kolaborator, kolaborator: formData.kolaborator,
}; };
await kolaborasiState.update.submit(); await kolaborasiState.update.submit();
toast.success("Berita berhasil diperbarui!"); toast.success("Kolaborasi inovasi berhasil diperbarui!");
router.push("/admin/inovasi/kolaborasi-inovasi"); router.push("/admin/inovasi/kolaborasi-inovasi");
} catch (error) { } catch (error) {
console.error("Error updating berita:", error); console.error("Error updating kolaborasi:", error);
toast.error("Terjadi kesalahan saat memperbarui berita"); toast.error("Terjadi kesalahan saat memperbarui data");
} }
}; };
return ( return (
<Box> <Box px={{ base: "sm", md: "lg" }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors["blue-button"]} size={30} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors["blue-button"]} size={24} />
</Box> </Button>
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> </Tooltip>
<Stack gap={"xs"}> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Kolaborasi Inovasi</Title> Edit Kolaborasi Inovasi
</Title>
</Group>
<Paper
w={{ base: "100%", md: "60%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Kolaborasi"
placeholder="Masukkan nama kolaborasi"
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} required
placeholder="masukkan nama"
/> />
<TextInput <TextInput
label="Deskripsi Singkat"
placeholder="Masukkan deskripsi singkat"
value={formData.slug} value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })} onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat</Text>} required
placeholder="masukkan deskripsi singkat"
/> />
<TextInput <TextInput
label="Tahun"
placeholder="Masukkan tahun"
value={formData.tahun} value={formData.tahun}
onChange={(e) => setFormData({ ...formData, tahun: e.target.value })} onChange={(e) => setFormData({ ...formData, tahun: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tahun</Text>} required
placeholder="masukkan tahun"
/> />
<TextInput <TextInput
label="Kolaborator"
placeholder="Masukkan nama kolaborator"
value={formData.kolaborator} value={formData.kolaborator}
onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })} onChange={(e) => setFormData({ ...formData, kolaborator: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Kolaborator</Text>} required
placeholder="masukkan kolaborator"
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Konten</Text> <Text fw="bold" fz="sm" mb={6}>
Konten
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -126,7 +145,21 @@ function EditKolaborasiInovasi() {
}} }}
/> />
</Box> </Box>
<Button onClick={handleSubmit}>Simpan</Button>
<Group justify="right">
<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,115 +1,140 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
function DetailKolaborasiInovasi() { function DetailKolaborasiInovasi() {
const kolaborasiState = useProxy(kolaborasiInovasiState) const kolaborasiState = useProxy(kolaborasiInovasiState);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
kolaborasiState.findUnique.load(params?.id as string) kolaborasiState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
kolaborasiState.delete.byId(selectedId) kolaborasiState.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/inovasi/kolaborasi-inovasi") router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
} }
} };
if (!kolaborasiState.findUnique.data) { if (!kolaborasiState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = kolaborasiState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Kolaborasi Inovasi</Text> Kembali
{kolaborasiState.findUnique.data ? ( </Button>
<Paper key={kolaborasiState.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Card utama */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Nama Kolaborasi Inovasi</Text> withBorder
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.name}</Text> w={{ base: "100%", md: "70%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Tahun</Text> radius="md"
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.tahun}</Text> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fw={"bold"} fz={"lg"}>Deskripsi Singkat</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} >{kolaborasiState.findUnique.data?.slug}</Text> Detail Kolaborasi Inovasi
</Box> </Text>
<Box>
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> {/* Isi detail */}
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kolaborasiState.findUnique.data?.deskripsi }} /> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
</Box> <Stack gap="sm">
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Kolaborator</Text> <Text fz="lg" fw="bold">Nama Kolaborasi Inovasi</Text>
<Text fz={"lg"}>{kolaborasiState.findUnique.data?.kolaborator}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Flex gap={"xs"} mt={10}>
<Box>
<Text fz="lg" fw="bold">Tahun</Text>
<Text fz="md" c="dimmed">{data?.tahun || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Kolaborator</Text>
<Text fz="md" c="dimmed">{data?.kolaborator || '-'}</Text>
</Box>
{/* Tombol aksi */}
<Group gap="sm">
<Tooltip label="Hapus Kolaborasi Inovasi" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (kolaborasiState.findUnique.data) { setSelectedId(data.id);
setSelectedId(kolaborasiState.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={kolaborasiState.delete.loading || !kolaborasiState.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={kolaborasiState.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Kolaborasi Inovasi" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (kolaborasiState.findUnique.data) { onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${data.id}/edit`)}
router.push(`/admin/inovasi/kolaborasi-inovasi/${kolaborasiState.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!kolaborasiState.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Konfirmasi Hapus */} {/* Modal konfirmasi hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus kolaborasi inovasi ini?' text="Apakah anda yakin ingin menghapus kolaborasi inovasi ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,7 +3,7 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi'; import kolaborasiInovasiState from '@/app/admin/(dashboard)/_state/inovasi/kolaborasi-inovasi';
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 { YearPickerInput } from '@mantine/dates'; import { YearPickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -11,10 +11,8 @@ import { useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateProgramKreatifDesa() { function CreateProgramKreatifDesa() {
const stateCreate = useProxy(kolaborasiInovasiState) const stateCreate = useProxy(kolaborasiInovasiState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
@@ -24,10 +22,10 @@ function CreateProgramKreatifDesa() {
slug: "", slug: "",
deskripsi: "", deskripsi: "",
kolaborator: "", kolaborator: "",
} };
} };
// Generate slug from name // Generate slug dari name
useEffect(() => { useEffect(() => {
const { name } = stateCreate.create.form; const { name } = stateCreate.create.form;
if (name) { if (name) {
@@ -42,67 +40,89 @@ function CreateProgramKreatifDesa() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// Submit data kolaborasi inovasi
await stateCreate.create.create(); await stateCreate.create.create();
// Reset form setelah submit
resetForm(); resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi"); router.push("/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi");
toast.success("Berhasil menambahkan kolaborasi inovasi"); toast.success("Berhasil menambahkan kolaborasi inovasi");
} catch (error) { } catch (error) {
console.error("Error creating kolaborasi inovasi:", error); console.error("Error creating kolaborasi inovasi:", error);
toast.error("Terjadi kesalahan saat menyimpan data"); toast.error("Terjadi kesalahan saat menyimpan data");
} }
} };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</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 Kolaborasi Inovasi</Title> {/* Back Button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" 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">
Tambah Kolaborasi Inovasi
</Title>
</Group>
{/* Form */}
<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 Kolaborasi Inovasi</Text>} label={<Text fz="sm" fw="bold">Nama Kolaborasi Inovasi</Text>}
placeholder="masukkan nama kolaborasi inovasi" placeholder="Masukkan nama kolaborasi inovasi"
value={stateCreate.create.form.name || ''}
onChange={(val) => stateCreate.create.form.name = val.target.value} onChange={(val) => stateCreate.create.form.name = val.target.value}
required
/> />
<YearPickerInput <YearPickerInput
clearable clearable
value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null} value={stateCreate.create.form.tahun ? new Date(stateCreate.create.form.tahun, 0, 1) : null}
label="Tahun" label={<Text fz="sm" fw="bold">Tahun</Text>}
placeholder="Pilih tahun" placeholder="Pilih tahun"
onChange={(dateString: string) => { onChange={(date) => {
const year = dateString ? new Date(dateString).getFullYear() : 0; const year = date ? new Date(date).getFullYear() : 0;
stateCreate.create.form.tahun = year; stateCreate.create.form.tahun = year;
}} }}
/> />
<Box> <Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<CreateEditor <CreateEditor
value={stateCreate.create.form.deskripsi} value={stateCreate.create.form.deskripsi}
onChange={(val) => { onChange={(val) => stateCreate.create.form.deskripsi = val}
stateCreate.create.form.deskripsi = val;
}}
/> />
</Box> </Box>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kolaborator</Text>}
placeholder="Masukkan kolaborator"
value={stateCreate.create.form.kolaborator || ''}
onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value} onChange={(e) => stateCreate.create.form.kolaborator = e.currentTarget.value}
label={<Text fw={"bold"} fz={"sm"}>Kolaborator</Text>}
placeholder='Masukkan kolaborator'
/> />
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Kolaborasi Inovasi</Text> <Group justify="right" mt="md">
<CreateEditor <Button
value={stateCreate.create.form.deskripsi} onClick={handleSubmit}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent} radius="md"
/> size="md"
</Box> style={{
<Group> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,10 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi'; import kolaborasiInovasiState from '../../../_state/inovasi/kolaborasi-inovasi';
@@ -15,8 +32,8 @@ function KolaborasiInovasi() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Kolaborasi Inovasi' title="Kolaborasi Inovasi"
placeholder='pencarian' placeholder="Cari kolaborasi inovasi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,101 +44,116 @@ function KolaborasiInovasi() {
} }
function ListKolaborasiInovasi({ search }: { search: string }) { function ListKolaborasiInovasi({ search }: { search: string }) {
const listState = useProxy(kolaborasiInovasiState) const listState = useProxy(kolaborasiInovasiState);
const router = useRouter(); const router = useRouter();
const { const { data, loading, page, totalPages, load } = listState.findMany;
data,
loading,
page,
totalPages,
load,
} = listState.findMany
useEffect(() => { useEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={650} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Kolaborasi Inovasi'
href='/admin/inovasi/kolaborasi-inovasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Kolaborasi Inovasi</TableTh>
<TableTh>Tahun</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Kolaborasi Inovasi' <Title order={4}>Daftar Kolaborasi Inovasi</Title>
href='/admin/inovasi/kolaborasi-inovasi/create' <Tooltip label="Tambah Kolaborasi Inovasi" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>No</TableTh> onClick={() => router.push('/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/create')}
<TableTh>Nama Kolaborasi Inovasi</TableTh> >
<TableTh>Tahun</TableTh> Tambah Baru
<TableTh>Deskripsi Singkat</TableTh> </Button>
<TableTh>Detail</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead> <Box style={{ overflowX: 'auto' }}>
<TableTbody> <Table highlightOnHover>
{filteredData.map((item, index) => ( <TableThead>
<TableTr key={item.id}> <TableTr>
<TableTd>{index + 1}</TableTd> <TableTh style={{ width: '5%' }}>No</TableTh>
<TableTd>{item.name}</TableTd> <TableTh style={{ width: '25%' }}>Nama Kolaborasi Inovasi</TableTh>
<TableTd>{item.tahun}</TableTd> <TableTh style={{ width: '15%' }}>Tahun</TableTh>
<TableTd>{item.slug}</TableTd> <TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTd> <TableTh style={{ width: '10%' }}>Aksi</TableTh>
<Button onClick={() => router.push(`/admin/inovasi/kolaborasi-inovasi/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" c="dimmed">
{item.tahun}
</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate="end" lineClamp={1} c="dimmed">
{item.slug}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() =>
router.push(`/admin/inovasi/kolaborasi-inovasi/list-kolaborasi-inovasi/${item.id}`)
}
>
Detail
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">Tidak ada data kolaborasi inovasi yang tersedia</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box > </Box>
); );
} }

View File

@@ -3,24 +3,41 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi'; import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconImageInPicture, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import {
IconArrowBack,
IconImageInPicture,
IconPhoto,
IconUpload,
IconX,
} 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 { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditMitraKolaborasi() {
function EditFoto() { const state = useProxy(mitraKolaborasi);
const state = useProxy(mitraKolaborasi)
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: state.update.form.name || '', name: state.update.form.name || '',
imageId: state.update.form.imageId || '' imageId: state.update.form.imageId || '',
}); });
useEffect(() => { useEffect(() => {
@@ -32,7 +49,7 @@ function EditFoto() {
if (data) { if (data) {
setFormData({ setFormData({
name: data.name || '', name: data.name || '',
imageId: data.imageId || '' imageId: data.imageId || '',
}); });
if (data?.image?.link) { if (data?.image?.link) {
setPreviewImage(data.image.link); setPreviewImage(data.image.link);
@@ -51,7 +68,7 @@ function EditFoto() {
state.update.form = { state.update.form = {
...state.update.form, ...state.update.form,
name: formData.name, name: formData.name,
imageId: formData.imageId imageId: formData.imageId,
}; };
if (file) { if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -60,7 +77,7 @@ function EditFoto() {
}); });
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal upload gambar');
} }
state.update.form.imageId = uploaded.id; state.update.form.imageId = uploaded.id;
} }
@@ -74,70 +91,114 @@ function EditFoto() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Mitra
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form Card */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Mitra</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>} label="Nama Mitra"
placeholder='Masukkan nama mitra' placeholder="Masukkan nama mitra"
value={formData.name} value={formData.name}
onChange={(e) => onChange={(e) => setFormData({ ...formData, name: e.target.value })}
(formData.name = e.target.value) required
}
/> />
{/* Upload Foto */}
<Box> <Box>
<Text>Upload Foto</Text> <Text fw="bold" fz="sm" mb={6}>
Upload Foto
</Text>
<Dropzone <Dropzone
onDrop={(files) => { onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama const selectedFile = files[0];
if (selectedFile) { if (selectedFile) {
setFile(selectedFile); setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setPreviewImage(URL.createObjectURL(selectedFile));
} }
}} }}
onReject={() => toast.error('File tidak valid.')} onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
radius="md"
p="xl"
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Idle>
<Stack gap="xs" align="center">
<div> <Text size="md" fw={500}>
<Text size="xl" inline> Seret gambar atau klik untuk memilih file
Drag gambar ke sini atau klik untuk pilih file
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size="sm" c="dimmed">
Maksimal 5MB dan harus format gambar Maksimal 5MB, format gambar wajib
</Text> </Text>
</div> </Stack>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Preview Foto */}
{previewImage ? ( {previewImage ? (
<Image alt="" src={previewImage} w={200} h={200} loading="lazy"/> <Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 220,
objectFit: 'contain',
border: `1px solid ${colors['blue-button']}`,
}}
loading="lazy"
/>
</Box>
) : ( ) : (
<Center w={200} h={200} bg={"gray"}> <Center w={200} h={200} bg="gray.1" mt="sm" style={{ borderRadius: 8 }}>
<IconImageInPicture /> <IconImageInPicture size={48} color="#868e96" />
</Center> </Center>
)} )}
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> {/* Submit */}
<Group justify="right">
<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>
@@ -145,4 +206,4 @@ function EditFoto() {
); );
} }
export default EditFoto; export default EditMitraKolaborasi;

View File

@@ -2,7 +2,18 @@
import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi'; import mitraKolaborasi from '@/app/admin/(dashboard)/_state/inovasi/mitra-kolaborasi';
import colors from '@/con/colors'; import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch'; import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -10,27 +21,24 @@ import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateMitraKolaborasi() {
const state = useProxy(mitraKolaborasi);
function CreateFoto() {
const state = useProxy(mitraKolaborasi)
const router = useRouter(); const router = useRouter();
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
name: "", name: '',
imageId: "", imageId: '',
}; };
setPreviewImage(null);
setPreviewImage(null) setFile(null);
setFile(null)
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Silakan pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -40,93 +48,110 @@ function CreateFoto() {
const uploaded = res.data?.data; const uploaded = res.data?.data;
if (!uploaded?.id) { if (!uploaded?.id) {
return toast.error("Gagal upload gambar"); return toast.error('Gagal mengunggah gambar, silakan coba lagi');
} }
state.create.form.imageId = uploaded.id; state.create.form.imageId = uploaded.id;
await state.create.create(); await state.create.create();
resetForm(); resetForm();
router.push("/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi") router.push('/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi');
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Back Button + Title */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Mitra Kolaborasi
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Wrapper */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Mitra</Title> w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Input Nama Mitra */}
<TextInput <TextInput
label={<Text fw={"bold"} fz={"sm"}>Nama Mitra</Text>} label="Nama Mitra"
placeholder='Masukkan nama mitra' placeholder="Masukkan nama mitra"
value={state.create.form.name} value={state.create.form.name || ''}
onChange={(val) => { onChange={(e) => (state.create.form.name = e.target.value)}
state.create.form.name = val.target.value; required
}}
/> />
{/* Upload Image */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm" mb={6}>
<Box> Gambar Mitra
<Dropzone </Text>
onDrop={(files) => { <Dropzone
const selectedFile = files[0]; // Ambil file pertama onDrop={(files) => {
if (selectedFile) { const selectedFile = files[0];
setFile(selectedFile); if (selectedFile) {
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview setFile(selectedFile);
} setPreviewImage(URL.createObjectURL(selectedFile));
}} }
onReject={() => toast.error('File tidak valid.')} }}
maxSize={5 * 1024 ** 2} // Maks 5MB onReject={() => toast.error('File tidak valid, gunakan format gambar')}
accept={{ 'image/*': [] }} maxSize={5 * 1024 ** 2}
> accept={{ 'image/*': [] }}
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> radius="md"
<Dropzone.Accept> p="xl"
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> >
</Dropzone.Accept> <Group justify="center" gap="xl" mih={180}>
<Dropzone.Reject> <Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Reject> </Dropzone.Accept>
<Dropzone.Idle> <Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Idle> </Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={48} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
</Group>
<Text ta="center" mt="sm" size="sm" color="dimmed">
Seret gambar atau klik untuk memilih file (maks 5MB)
</Text>
</Dropzone>
<div> {previewImage && (
<Text size="xl" inline> <Box mt="sm" style={{ textAlign: 'center' }}>
Drag gambar ke sini atau klik untuk pilih file <Image
</Text> src={previewImage}
<Text size="sm" c="dimmed" inline mt={7}> alt="Preview Gambar"
Maksimal 5MB dan harus format gambar radius="md"
</Text> style={{ maxHeight: 200, objectFit: 'contain', border: '1px solid #ddd' }}
</div> loading="lazy"
</Group> />
</Dropzone> </Box>
)}
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading="lazy"
/>
</Box>
)}
</Box>
</Box> </Box>
<Group>
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> {/* Submit Button */}
<Group justify="right">
<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>
@@ -134,4 +159,4 @@ function CreateFoto() {
); );
} }
export default CreateFoto; export default CreateMitraKolaborasi;

View File

@@ -1,13 +1,31 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import {
import { IconEdit, IconSearch, IconX } from '@tabler/icons-react'; Box,
Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconEdit, IconSearch, IconX, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { 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 HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi'; import mitraKolaborasi from '../../../_state/inovasi/mitra-kolaborasi';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
@@ -16,8 +34,8 @@ function MitraKolaborasi() {
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Mitra Kolaborasi' title="Mitra Kolaborasi"
placeholder='pencarian' placeholder="Cari nama mitra..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,160 +46,169 @@ function MitraKolaborasi() {
} }
function ListMitraKolaborasi({ search }: { search: string }) { function ListMitraKolaborasi({ search }: { search: string }) {
const listState = useProxy(mitraKolaborasi) const listState = useProxy(mitraKolaborasi);
const router = useRouter(); const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
mitraKolaborasi.delete.byId(selectedId) mitraKolaborasi.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/inovasi/kolaborasi-inovasi") router.push('/admin/inovasi/kolaborasi-inovasi');
} }
} };
const { const { data, loading, page, totalPages, load } = listState.findMany;
data,
loading,
page,
totalPages,
load,
} = listState.findMany
useEffect(() => { useEffect(() => {
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={650} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) {
return (
<Box py={10}>
<Paper p="md" >
<Stack>
<JudulList
title='List Mitra Kolaborasi'
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Mitra</TableTh>
<TableTh>Image</TableTh>
<TableTh>Delete</TableTh>
<TableTh>Edit</TableTh>
</TableTr>
</TableThead>
</Table>
<Text ta="center">Tidak ada data mitra kolaborasi yang tersedia</Text>
</Stack>
</Paper>
</Box >
);
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Mitra Kolaborasi' <Title order={4}>Daftar Mitra Kolaborasi</Title>
href='/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create' <Tooltip label="Tambah Mitra Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>No</TableTh> onClick={() =>
<TableTh>Nama Mitra</TableTh> router.push(
<TableTh>Image</TableTh> '/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/create'
<TableTh>Delete</TableTh> )
<TableTh>Edit</TableTh> }
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item, index) => ( </Tooltip>
<TableTr key={item.id}> </Group>
<TableTd>{index + 1}</TableTd> <Box style={{ overflowX: 'auto' }}>
<TableTd>{item.name}</TableTd> <Table highlightOnHover>
<TableTd> <TableThead>
<Box style={{ <TableTr>
width: 100, <TableTh style={{ width: '10%' }}>No</TableTh>
height: 100, <TableTh style={{ width: '30%' }}>Nama Mitra</TableTh>
position: 'relative', <TableTh style={{ width: '25%' }}>Image</TableTh>
overflow: 'hidden', <TableTh style={{ width: '15%' }}>Delete</TableTh>
borderRadius: 4 <TableTh style={{ width: '15%' }}>Edit</TableTh>
}}>
<Image
src={item.image?.link || ''}
alt={item.name}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center'
}}
loading="lazy"
/>
</Box>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={mitraKolaborasi.delete.loading || !item}
color={"red"}
>
<IconX size={20} />
</Button>
</TableTd>
<TableTd>
<Button
onClick={() => {
if (item) {
router.push(`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`);
}
}}
disabled={!item}
color={"green"}
>
<IconEdit size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>{index + 1}</TableTd>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>
<Box
w={70}
h={70}
>
{item.image?.link ? (
<Image
loading="lazy"
src={item.image.link}
alt={item.name}
fit="cover"
/>
) : (
<Box bg={colors['blue-button']} w="100%" h="100%" />
)}
</Box>
</TableTd>
<TableTd>
<Tooltip label="Hapus Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="red"
disabled={mitraKolaborasi.delete.loading || !item}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconX size={16} />
</Button>
</Tooltip>
</TableTd>
<TableTd>
<Tooltip label="Edit Mitra" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="green"
disabled={!item}
onClick={() =>
router.push(
`/admin/inovasi/kolaborasi-inovasi/mitra-kolaborasi/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data mitra kolaborasi yang tersedia
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus mitra kolaborasi ini?' text="Apakah anda yakin ingin menghapus mitra kolaborasi ini?"
/> />
</Box > </Box>
); );
} }

View File

@@ -4,7 +4,6 @@ import {
Box, Box,
Button, Button,
Center, Center,
Group,
Pagination, Pagination,
Paper, Paper,
Skeleton, Skeleton,
@@ -16,11 +15,10 @@ import {
TableThead, TableThead,
TableTr, TableTr,
Text, Text,
Title, Title
Tooltip
} from '@mantine/core'; } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } 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';
@@ -66,27 +64,13 @@ function ListAdministrasiOnline({ search }: { search: string }) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md"> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Administrasi Online</Title> <Title order={4}>Daftar Administrasi Online</Title>
<Tooltip label="Tambah Layanan Baru" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/inovasi/layanan-online-desa/administrasi-online/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}> <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama Layanan</TableTh> <TableTh style={{ width: '25%' }}>Nama</TableTh>
<TableTh style={{ width: '25%' }}>Alamat</TableTh> <TableTh style={{ width: '25%' }}>Alamat</TableTh>
<TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh> <TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTh style={{ width: '15%' }}>Aksi</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -32,60 +32,88 @@ function DetailJenisLayanan() {
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
const data = state.findUnique.data
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Jenis Layanan</Text> Kembali
{state.findUnique.data ? ( </Button>
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Card utama */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Nama</Text> withBorder
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text> w={{ base: "100%", md: "60%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Deskripsi</Text> radius="md"
<Text fz={"lg"}dangerouslySetInnerHTML={{ __html: state.findUnique.data?.deskripsi }}></Text> shadow="sm"
</Box> >
<Flex gap={"xs"} mt={10}> <Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Jenis Layanan
</Text>
{/* Detail isi */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }}
/>
</Box>
{/* Tombol aksi */}
<Group gap="sm" mt="sm">
<Tooltip label="Hapus Jenis Layanan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (state.findUnique.data) { setSelectedId(data.id)
setSelectedId(state.findUnique.data.id); setModalHapus(true)
setModalHapus(true);
}
}} }}
disabled={state.delete.loading || !state.findUnique.data} variant="light"
color={"red"} radius="md"
size="md"
disabled={state.delete.loading}
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Jenis Layanan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (state.findUnique.data) { onClick={() => router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${data.id}/edit`)}
router.push(`/admin/inovasi/layanan-online-desa/jenis-layanan/${state.findUnique.data.id}/edit`); variant="light"
} radius="md"
}} size="md"
disabled={!state.findUnique.data}
color={"green"}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -94,7 +122,7 @@ function DetailJenisLayanan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus jenis layanan ini?' text='Apakah Anda yakin ingin menghapus jenis layanan ini?'
/> />
</Box> </Box>
); );

View File

@@ -2,7 +2,17 @@
'use client' 'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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 { useEffect } from 'react'; import { useEffect } from 'react';
@@ -10,7 +20,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisLayanan() { function CreateJenisLayanan() {
const router = useRouter(); const router = useRouter();
const statePasar = useProxy(layananonlineDesa.jenisLayanan) const statePasar = useProxy(layananonlineDesa.jenisLayanan);
useEffect(() => { useEffect(() => {
statePasar.findMany.load(); statePasar.findMany.load();
@@ -18,51 +28,81 @@ function CreateJenisLayanan() {
const resetForm = () => { const resetForm = () => {
statePasar.create.form = { statePasar.create.form = {
nama: "", nama: '',
deskripsi: "", deskripsi: '',
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await statePasar.create.create(); await statePasar.create.create();
resetForm(); resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-layanan") router.push('/admin/inovasi/layanan-online-desa/jenis-layanan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> {/* Header dengan tombol back */}
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Layanan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Jenis Layanan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={statePasar.create.form.nama} p="lg"
onChange={(val) => { radius="md"
statePasar.create.form.nama = val.target.value; shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={statePasar.create.form.nama}
onChange={(val) => {
statePasar.create.form.nama = val.target.value;
}}
label={<Text fw="bold" fz="sm">Nama Jenis Layanan</Text>}
placeholder="Masukkan nama jenis layanan"
required
/>
<TextInput
value={statePasar.create.form.deskripsi}
onChange={(val) => {
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw="bold" fz="sm">Deskripsi</Text>}
placeholder="Masukkan deskripsi"
required
/>
<Group justify="right">
<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)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Layanan</Text>} >
placeholder='Masukkan nama jenis layanan' Simpan
/> </Button>
<TextInput </Group>
value={statePasar.create.form.deskripsi} </Stack>
onChange={(val) => { </Paper>
statePasar.create.form.deskripsi = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi</Text>}
placeholder='Masukkan deskripsi'
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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,
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';
@@ -70,24 +79,55 @@ function EditJenisPengaduan() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header + tombol back */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> 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 Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Card Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Edit Jenis Pengaduan</Title> 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
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>} label="Nama Jenis Pengaduan"
placeholder='Masukkan nama jenis pengaduan' placeholder="Masukkan nama jenis pengaduan"
required
/> />
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <Group justify="right">
<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

@@ -2,7 +2,16 @@
'use client' 'use client'
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
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,
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 { useEffect } from 'react'; import { useEffect } from 'react';
@@ -10,7 +19,7 @@ import { useProxy } from 'valtio/utils';
function CreateJenisPengaduan() { function CreateJenisPengaduan() {
const router = useRouter(); const router = useRouter();
const state = useProxy(layananonlineDesa.jenisPengaduan) const state = useProxy(layananonlineDesa.jenisPengaduan);
useEffect(() => { useEffect(() => {
state.findMany.load(); state.findMany.load();
@@ -18,42 +27,64 @@ function CreateJenisPengaduan() {
const resetForm = () => { const resetForm = () => {
state.create.form = { state.create.form = {
nama: "", nama: '',
}; };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await state.create.create(); await state.create.create();
resetForm(); resetForm();
router.push("/admin/inovasi/layanan-online-desa/jenis-pengaduan") router.push('/admin/inovasi/layanan-online-desa/jenis-pengaduan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box> {/* Header */}
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack color={colors['blue-button']} size={25} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> </Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Pengaduan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> {/* Form */}
<Stack gap={"xs"}> <Paper
<Title order={4}>Create Jenis Pengaduan</Title> w={{ base: '100%', md: '50%' }}
<TextInput bg={colors['white-1']}
value={state.create.form.nama} p="lg"
onChange={(val) => { radius="md"
state.create.form.nama = val.target.value; shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Jenis Pengaduan"
placeholder="Masukkan nama jenis pengaduan"
value={state.create.form.nama || ''}
onChange={(e) => (state.create.form.nama = e.target.value)}
required
/>
<Group justify="right">
<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)',
}} }}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pengaduan</Text>} >
placeholder='Masukkan nama jenis pengaduan' Simpan
/> </Button>
<Group> </Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> </Stack>
</Group> </Paper>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,25 +1,40 @@
'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 { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconSearch, IconTrash } 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 layananonlineDesa from '../../../_state/inovasi/layanan-online-desa'; import layananonlineDesa from '../../../_state/inovasi/layanan-online-desa';
function JenisPengaduan() { function JenisPengaduan() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Jenis Pengaduan' title="Jenis Pengaduan"
placeholder='pencarian' placeholder="Cari nama jenis pengaduan..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,87 +45,138 @@ function JenisPengaduan() {
} }
function ListJenisPengaduan({ search }: { search: string }) { function ListJenisPengaduan({ search }: { search: string }) {
const state = useProxy(layananonlineDesa.jenisPengaduan) const state = useProxy(layananonlineDesa.jenisPengaduan);
const router = useRouter() const router = useRouter();
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const { data, page, totalPages, loading, load, } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
state.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const handleHapus = async () => { const handleHapus = async () => {
if (selectedId) { if (selectedId) {
await state.delete.byId(selectedId); await state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
} }
} };
const filteredData = data || []
const filteredData = (state.findMany.data || []).filter(item => { if (loading || !data) {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword)
);
});
if (!state.findMany.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Jenis Pengaduan' <Title order={4}>Daftar Jenis Pengaduan</Title>
href='/admin/inovasi/layanan-online-desa/jenis-pengaduan/create' <Tooltip label="Tambah Jenis Pengaduan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Nama Jenis Pengaduan</TableTh> onClick={() =>
<TableTh>Edit</TableTh> router.push(
<TableTh>Hapus</TableTh> '/admin/inovasi/layanan-online-desa/jenis-pengaduan/create'
</TableTr> )
</TableThead> }
<TableTbody> >
{filteredData.map((item) => ( Tambah Baru
<TableTr key={item.id}> </Button>
<TableTd>{item.nama}</TableTd> </Tooltip>
<TableTd> </Group>
<Button color="green"
onClick={() => { <Box style={{ overflowX: 'auto' }}>
if (item) { <Table highlightOnHover>
router.push(`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`); <TableThead>
} <TableTr>
}} <TableTh style={{ width: '60%' }}>Nama Jenis Pengaduan</TableTh>
> <TableTh style={{ width: '20%' }}>Edit</TableTh>
<IconEdit size={20} /> <TableTh style={{ width: '20%' }}>Hapus</TableTh>
</Button>
</TableTd>
<TableTd>
<Button color="red"
onClick={() => {
if (item) {
setSelectedId(item.id);
setModalHapus(true);
}
}}
disabled={!item}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.nama}
</Text>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="green"
leftSection={<IconEdit size={16} />}
onClick={() =>
router.push(
`/admin/inovasi/layanan-online-desa/jenis-pengaduan/${item.id}`
)
}
>
Edit
</Button>
</TableTd>
<TableTd>
<Button
size="xs"
radius="md"
variant="light"
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
Hapus
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">
Tidak ada jenis pengaduan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Hapus */} {/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}

View File

@@ -1,7 +1,6 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { Box, Button, Group, Image, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { Box, Button, Flex, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconTrash } from '@tabler/icons-react'; import { IconArrowBack, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -11,102 +10,143 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa'; import layananonlineDesa from '@/app/admin/(dashboard)/_state/inovasi/layanan-online-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
function DetailPengaduanMasyarakat() { function DetailPengaduanMasyarakat() {
const pengaduanState = useProxy(layananonlineDesa) const pengaduanState = useProxy(layananonlineDesa);
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
const params = useParams() const params = useParams();
const router = useRouter() const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string) pengaduanState.pengaduanMasyarakat.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
pengaduanState.pengaduanMasyarakat.delete.byId(selectedId) pengaduanState.pengaduanMasyarakat.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat") router.push("/admin/inovasi/layanan-online-desa/pengaduan-masyarakat");
} }
} };
if (!pengaduanState.pengaduanMasyarakat.findUnique.data) { if (!pengaduanState.pengaduanMasyarakat.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = pengaduanState.pengaduanMasyarakat.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> mb={15}
<Stack> >
<Flex gap={"xs"} justify={"space-between"} mt={10}> Kembali
<Text fz={"xl"} fw={"bold"}>Detail Pengaduan Masyarakat</Text> </Button>
<Button
onClick={() => { {/* Card Detail */}
if (pengaduanState.pengaduanMasyarakat.findUnique.data) { <Paper
setSelectedId(pengaduanState.pengaduanMasyarakat.findUnique.data.id); withBorder
setModalHapus(true); w={{ base: "100%", md: "60%" }}
} bg={colors['white-1']}
}} p="lg"
disabled={pengaduanState.pengaduanMasyarakat.delete.loading || !pengaduanState.pengaduanMasyarakat.findUnique.data} radius="md"
color={"red"} shadow="sm"
> >
<IconTrash size={20} /> <Stack gap="md">
</Button> {/* Judul Halaman */}
</Flex> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
{pengaduanState.pengaduanMasyarakat.findUnique.data ? ( Detail Pengaduan Masyarakat
<Paper key={pengaduanState.pengaduanMasyarakat.findUnique.data.id} bg={colors['BG-trans']} p={'md'}> </Text>
<Stack gap={"xs"}>
<Box> {/* Isi Data */}
<Text fw={"bold"} fz={"lg"}>Nama</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.name}</Text> <Stack gap="sm">
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Nama</Text>
<Text fw={"bold"} fz={"lg"}>Email</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
<Text fz={"lg"}>{pengaduanState.pengaduanMasyarakat.findUnique.data?.email}</Text> </Box>
</Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Nomor Telepon</Text> <Text fz="lg" fw="bold">Email</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nomorTelepon}</Text> <Text fz="md" c="dimmed">{data?.email || '-'}</Text>
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text> <Box>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.nik}</Text> <Text fz="lg" fw="bold">Nomor Telepon</Text>
</Box> <Text fz="md" c="dimmed">{data?.nomorTelepon || '-'}</Text>
<Box> </Box>
<Text fw={"bold"} fz={"lg"}>Judul Pengaduan</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.judulPengaduan}</Text> <Box>
</Box> <Text fz="lg" fw="bold">NIK</Text>
<Box> <Text fz="md" c="dimmed">{data?.nik || '-'}</Text>
<Text fw={"bold"} fz={"lg"}>Lokasi Kejadian</Text> </Box>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.lokasiKejadian}</Text>
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Judul Pengaduan</Text>
<Text fw={"bold"} fz={"lg"}>Deskripsi Pengaduan</Text> <Text fz="md" c="dimmed">{data?.judulPengaduan || '-'}</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.deskripsiPengaduan}</Text> </Box>
</Box>
<Box> <Box>
<Text fw={"bold"} fz={"lg"}>Jenis Pengaduan</Text> <Text fz="lg" fw="bold">Lokasi Kejadian</Text>
<Text fz={"lg"} >{pengaduanState.pengaduanMasyarakat.findUnique.data?.jenisPengaduan?.nama}</Text> <Text fz="md" c="dimmed">{data?.lokasiKejadian || '-'}</Text>
</Box> </Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Gambar</Text> <Box>
<Image w={{ base: 150, md: 150, lg: 150 }} src={pengaduanState.pengaduanMasyarakat.findUnique.data?.image?.link} alt="gambar" loading="lazy"/> <Text fz="lg" fw="bold">Deskripsi Pengaduan</Text>
</Box> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsiPengaduan || '-' }} />
</Stack> </Box>
</Paper>
) : null} <Box>
<Text fz="lg" fw="bold">Jenis Pengaduan</Text>
<Text fz="md" c="dimmed">{data?.jenisPengaduan?.nama || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data?.image?.link ? (
<Image
src={data.image.link}
alt="Gambar Pengaduan"
w={120}
h={120}
radius="md"
fit="cover"
loading="lazy"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Action Button */}
<Group gap="sm">
<Tooltip label="Hapus Pengaduan" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
disabled={pengaduanState.pengaduanMasyarakat.delete.loading}
>
<IconTrash size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -115,7 +155,7 @@ function DetailPengaduanMasyarakat() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus administrasi online ini?' text="Apakah anda yakin ingin menghapus pengaduan masyarakat ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,6 +1,24 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
@@ -16,7 +34,7 @@ function PengaduanMasyarakat() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Pengaduan Masyarakat' title='Pengaduan Masyarakat'
placeholder='pencarian' placeholder='Cari nama, email, atau nomor telepon...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -27,7 +45,7 @@ function PengaduanMasyarakat() {
} }
function ListPengaduanMasyarakat({ search }: { search: string }) { function ListPengaduanMasyarakat({ search }: { search: string }) {
const listState = useProxy(layananonlineDesa.pengaduanMasyarakat) const listState = useProxy(layananonlineDesa.pengaduanMasyarakat);
const router = useRouter(); const router = useRouter();
const { const {
data, data,
@@ -38,58 +56,89 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
} = listState.findMany; } = listState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10); load(page, 10, search);
}, [page]); }, [page, search]);
const filteredData = (data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.email.toLowerCase().includes(keyword) ||
item.nomorTelepon.toLowerCase().includes(keyword)
);
});
if (loading || !data) { if (loading || !data) {
return <Skeleton h={500} />; return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={3} mb={10}>List Pengaduan Masyarakat</Title> <Group justify="space-between" mb="md">
<Table striped withTableBorder withRowBorders> <Title order={4}>Daftar Pengaduan Masyarakat</Title>
<TableThead> </Group>
<TableTr> <Box style={{ overflowX: "auto" }}>
<TableTh>Nama</TableTh> <Table highlightOnHover>
<TableTh>Email</TableTh> <TableThead>
<TableTh>Nomor Telepon</TableTh> <TableTr>
<TableTh>Detail</TableTh> <TableTh style={{ width: '25%' }}>Nama</TableTh>
</TableTr> <TableTh style={{ width: '25%' }}>Email</TableTh>
</TableThead> <TableTh style={{ width: '20%' }}>Nomor Telepon</TableTh>
<TableTbody style={{ overflowX: "auto" }}> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
{filteredData.map((item) => ( </TableTr>
<TableTr key={item.id}> </TableThead>
<TableTd>{item.name}</TableTd> <TableTbody>
<TableTd>{item.email}</TableTd> {filteredData.length > 0 ? (
<TableTd>{item.nomorTelepon}</TableTd> filteredData.map((item) => (
<TableTd> <TableTr key={item.id}>
<Button onClick={() => router.push(`/admin//inovasi/layanan-online-desa/administrasi-online/${item.id}`)}> <TableTd style={{ width: '25%' }}>
<IconDeviceImac size={20} /> <Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
</Button> </TableTd>
<TableTd style={{ width: '25%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.email}</Text>
</TableTd>
<TableTd style={{ width: '20%' }}>
<Text fz="sm" c="dimmed" truncate lineClamp={1}>{item.nomorTelepon}</Text>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Tooltip label="Lihat detail pengaduan" withArrow>
<Button
size="xs"
radius="md"
variant="light"
color="blue"
leftSection={<IconDeviceImac size={16} />}
onClick={() => router.push(`/admin/inovasi/layanan-online-desa/pengaduan-masyarakat/${item.id}`)}
>
Detail
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data pengaduan yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper> </Paper>
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>

View File

@@ -3,7 +3,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif'; import programKreatifState from '@/app/admin/(dashboard)/_state/inovasi/program-kreatif';
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';
@@ -19,17 +29,17 @@ interface FormProgramKreatif {
icon: string; icon: string;
} }
function EditProgramKreatifDesa() { function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState) const stateProgramKreatif = useProxy(programKreatifState);
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const [formData, setFormData] = useState<FormProgramKreatif>({ const [formData, setFormData] = useState<FormProgramKreatif>({
name: '', name: '',
deskripsi: '', deskripsi: '',
slug: '', slug: '',
icon: '', icon: '',
}) });
useEffect(() => { useEffect(() => {
const loadProgramKreatif = async () => { const loadProgramKreatif = async () => {
@@ -39,16 +49,13 @@ function EditProgramKreatifDesa() {
try { try {
const data = await stateProgramKreatif.update.load(id); const data = await stateProgramKreatif.update.load(id);
if (data) { if (data) {
// ⬇️ FIX PENTING: tambahkan ini
stateProgramKreatif.update.id = id; stateProgramKreatif.update.id = id;
stateProgramKreatif.update.form = { stateProgramKreatif.update.form = {
name: data.name, name: data.name,
slug: data.slug, slug: data.slug,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
icon: data.icon, icon: data.icon,
}; };
setFormData({ setFormData({
name: data.name, name: data.name,
slug: data.slug, slug: data.slug,
@@ -57,16 +64,14 @@ function EditProgramKreatifDesa() {
}); });
} }
} catch (error) { } catch (error) {
console.error("Error loading program kreatif:", error); console.error('Error loading program kreatif:', error);
toast.error("Gagal memuat data program kreatif"); toast.error('Gagal memuat data program kreatif');
} }
} };
loadProgramKreatif(); loadProgramKreatif();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateProgramKreatif.update.form = { stateProgramKreatif.update.form = {
@@ -75,49 +80,57 @@ function EditProgramKreatifDesa() {
deskripsi: formData.deskripsi.trim(), deskripsi: formData.deskripsi.trim(),
slug: formData.slug.trim(), slug: formData.slug.trim(),
icon: formData.icon.trim(), icon: formData.icon.trim(),
} };
await stateProgramKreatif.update.submit(); await stateProgramKreatif.update.submit();
router.push("/admin/inovasi/program-kreatif-desa"); router.push('/admin/inovasi/program-kreatif-desa');
} catch (error) { } catch (error) {
console.error("Error updating program kreatif:", error); console.error('Error updating program kreatif:', error);
toast.error("Gagal memuat data program kreatif"); toast.error('Gagal memuat data program kreatif');
} }
} };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> return (
<Stack gap={"xs"}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Title order={3}>Edit Program Kreatif Desa</Title> <Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" 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 Program Kreatif Desa
</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="Nama Program Kreatif Desa"
placeholder="Masukkan nama program kreatif desa"
value={formData.name} value={formData.name}
label={<Text fz={"sm"} fw={"bold"}>Nama Program Kreatif Desa</Text>} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="masukkan nama program kreatif desa" required
onChange={(val) => {
setFormData({
...formData,
name: val.target.value
})
}}
/> />
<TextInput <TextInput
label="Deskripsi Singkat Program Kreatif Desa"
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={formData.slug} value={formData.slug}
label={<Text fz={"sm"} fw={"bold"}>Deskripsi Singkat Program Kreatif Desa</Text>} onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
placeholder="masukkan deskripsi singkat program kreatif desa" required
onChange={(val) => {
setFormData({
...formData,
slug: val.target.value
})
}}
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={formData.deskripsi} value={formData.deskripsi}
onChange={(htmlContent) => { onChange={(htmlContent) => {
@@ -126,8 +139,11 @@ function EditProgramKreatifDesa() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text> <Text fw="bold" fz="sm" mb={6}>
Ikon Program Kreatif Desa
</Text>
<SelectIconProgramEdit <SelectIconProgramEdit
value={formData.icon as IconKey} value={formData.icon as IconKey}
onChange={(value) => { onChange={(value) => {
@@ -135,10 +151,22 @@ function EditProgramKreatifDesa() {
stateProgramKreatif.update.form.icon = value; stateProgramKreatif.update.form.icon = value;
}} }}
/> />
</Box> </Box>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Edit Berita</Button>
<Group justify="right">
<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 colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -10,8 +10,6 @@ import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKreatifState from '../../../_state/inovasi/program-kreatif'; import programKreatifState from '../../../_state/inovasi/program-kreatif';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKreatifDesa() { function DetailProgramKreatifDesa() {
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const stateProgramKreatif = useProxy(programKreatifState) const stateProgramKreatif = useProxy(programKreatifState)
@@ -34,74 +32,104 @@ function DetailProgramKreatifDesa() {
if (!stateProgramKreatif.findUnique.data) { if (!stateProgramKreatif.findUnique.data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
return ( const data = stateProgramKreatif.findUnique.data
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Program Kreatif Desa</Text>
<Paper bg={colors['BG-trans']} p={'md'}> return (
<Stack gap={"xs"}> <Box py={10}>
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
<Paper
withBorder
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Kreatif Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Program Kreatif Desa</Text> <Text fz="lg" fw="bold">Nama Program Kreatif Desa</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text> <Text fz="lg" fw="bold">Ikon Program Kreatif Desa</Text>
{stateProgramKreatif.findUnique.data?.icon && ( {data?.icon ? (
<IconMapper <IconMapper
name={stateProgramKreatif.findUnique.data?.icon as IconKey} name={data.icon as IconKey}
size={32} size={32}
color={colors['blue-button']} color={colors['blue-button']}
/> />
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)} )}
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz={"lg"}>{stateProgramKreatif.findUnique.data?.slug}</Text> <Text fz="md" c="dimmed">{data?.slug || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: stateProgramKreatif.findUnique.data?.deskripsi }}></Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.deskripsi || '-' }} />
</Box> </Box>
<Box>
<Flex gap={"xs"} mt={10}> <Flex gap="sm" mt={10}>
<Tooltip label="Hapus Program Kreatif Desa" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateProgramKreatif.findUnique.data) { if (data) {
setSelectedId(stateProgramKreatif.findUnique.data.id); setSelectedId(data.id);
setModalHapus(true); setModalHapus(true);
} }
}} }}
disabled={stateProgramKreatif.delete.loading || !stateProgramKreatif.findUnique.data} disabled={stateProgramKreatif.delete.loading || !data}
color={"red"} variant="light"
radius="md"
size="md"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Program Kreatif Desa" withArrow position="top">
<Button <Button
color="green"
onClick={() => { onClick={() => {
if (stateProgramKreatif.findUnique.data) { if (data) {
router.push(`/admin/inovasi/program-kreatif-desa/${stateProgramKreatif.findUnique.data.id}/edit`); router.push(`/admin/inovasi/program-kreatif-desa/${data.id}/edit`);
} }
}} }}
disabled={!stateProgramKreatif.findUnique.data} disabled={!data}
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -112,7 +140,7 @@ function DetailProgramKreatifDesa() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kreatif desa ini?" text="Apakah Anda yakin ingin menghapus program kreatif desa ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,6 +1,16 @@
'use client' 'use client'
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';
@@ -8,9 +18,8 @@ import CreateEditor from '../../../_com/createEditor';
import programKreatifState from '../../../_state/inovasi/program-kreatif'; import programKreatifState from '../../../_state/inovasi/program-kreatif';
import SelectIconProgram from '../../../_com/selectIcon'; import SelectIconProgram from '../../../_com/selectIcon';
function CreateProgramKreatifDesa() { function CreateProgramKreatifDesa() {
const stateCreate = useProxy(programKreatifState) const stateCreate = useProxy(programKreatifState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
@@ -19,48 +28,90 @@ function CreateProgramKreatifDesa() {
slug: "", slug: "",
deskripsi: "", deskripsi: "",
icon: "", icon: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateCreate.create.create(); await stateCreate.create.create();
resetForm(); resetForm();
router.push("/admin/inovasi/program-kreatif-desa") router.push("/admin/inovasi/program-kreatif-desa");
} };
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</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 Program Kreatif Desa</Title> {/* Tombol kembali */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" 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">
Tambah Program Kreatif Desa
</Title>
</Group>
{/* Card Form */}
<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 Program Kreatif Desa</Text>} label={<Text fw="bold" fz="sm">Nama Program Kreatif Desa</Text>}
placeholder="masukkan nama program kreatif desa" placeholder="Masukkan nama program kreatif desa"
onChange={(val) => stateCreate.create.form.name = val.target.value} value={stateCreate.create.form.name || ""}
onChange={(e) => (stateCreate.create.form.name = e.currentTarget.value)}
required
/> />
<Box> <Box>
<Text fz={"sm"} fw={"bold"}>Ikon Program Kreatif Desa</Text> <Text fw="bold" fz="sm" mb={6}>
<SelectIconProgram onChange={(value) => stateCreate.create.form.icon = value} /> Ikon Program Kreatif Desa
</Box> </Text>
<TextInput <SelectIconProgram
onChange={(e) => stateCreate.create.form.slug = e.currentTarget.value} onChange={(value) => (stateCreate.create.form.icon = value)}
label={<Text fw={"bold"} fz={"sm"}>Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder='Masukkan deskripsi singkat program kreatif desa'
/>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Program Kreatif Desa</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) => stateCreate.create.form.deskripsi = htmlContent}
/> />
</Box> </Box>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> <TextInput
label={<Text fw="bold" fz="sm">Deskripsi Singkat Program Kreatif Desa</Text>}
placeholder="Masukkan deskripsi singkat program kreatif desa"
value={stateCreate.create.form.slug || ""}
onChange={(e) => (stateCreate.create.form.slug = e.currentTarget.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Program Kreatif Desa
</Text>
<CreateEditor
value={stateCreate.create.form.deskripsi}
onChange={(htmlContent) =>
(stateCreate.create.form.deskripsi = htmlContent)
}
/>
</Box>
{/* Tombol Submit */}
<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,31 +1,64 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client'
import React from 'react';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconCash, IconChristmasTreeFilled, IconClipboard, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconHospital, IconScale, IconSchool, IconSearch, IconShieldFilled, IconShoppingCart, IconTrash, IconTree, IconTrendingUp, IconTruck } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import programKreatifState from '../../_state/inovasi/program-kreatif';
import { useProxy } from 'valtio/utils';
import { import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip
} from '@mantine/core';
import {
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled,
IconClipboard,
IconDeviceImac,
IconDroplet,
IconHome,
IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconPlus,
IconRecycle, IconRecycle,
IconScale,
IconSchool,
IconSearch,
IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrash,
IconTree,
IconTrendingUp,
IconTrophy, IconTrophy,
IconTruck,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKreatifState from '../../_state/inovasi/program-kreatif';
function ProgramKreatifDesa() { function ProgramKreatifDesa() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Program Kreatif Desa' title="Program Kreatif Desa"
placeholder='pencarian' placeholder="Cari program kreatif..."
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,23 +69,15 @@ function ProgramKreatifDesa() {
} }
function ListProgramKreatifDesa({ search }: { search: string }) { function ListProgramKreatifDesa({ search }: { search: string }) {
const listState = useProxy(programKreatifState) const listState = useProxy(programKreatifState);
const { data, loading, page, totalPages, load } = listState.findMany const { data, loading, page, totalPages, load } = listState.findMany;
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
load(page, 10) load(page, 10, search);
}, [page]) }, [page, search]);
const filteredData = (data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.slug.toLowerCase().includes(keyword) ||
item.icon.toLowerCase().includes(keyword)
);
});
const iconMap: Record<string, React.FC<any>> = { const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf, ekowisata: IconLeaf,
@@ -74,26 +99,40 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
bantuan: IconCash, bantuan: IconCash,
pelatihan: IconSchool, pelatihan: IconSchool,
subsidi: IconShoppingCart, subsidi: IconShoppingCart,
layananKesehatan: IconHospital layananKesehatan: IconHospital,
}; };
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton height={650} /> <Skeleton height={650} radius="md" />
</Stack> </Stack>
); );
} }
if (data.length === 0) { if (data.length === 0) {
return ( return (
<Box py={10}> <Box py={10}>
<Paper p="md" > <Paper p="md" radius="md" shadow="sm" withBorder>
<Stack> <Stack>
<JudulList <Group justify="space-between" mb="md">
title='List Program Kreatif Desa' <Title order={4}>Daftar Program Kreatif Desa</Title>
href='/admin/inovasi/program-kreatif-desa/create' <Tooltip label="Tambah Program Kreatif Desa" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Table highlightOnHover striped>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>No</TableTh> <TableTh>No</TableTh>
@@ -104,27 +143,52 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
</TableTr> </TableTr>
</TableThead> </TableThead>
</Table> </Table>
<Text ta="center">Tidak ada data program kreatif desa yang tersedia</Text> <Text ta="center" c="dimmed" py="lg">
Tidak ada data program kreatif desa yang tersedia
</Text>
</Stack> </Stack>
</Paper> </Paper>
</Box > </Box>
); );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'} h={{ base: 'auto', md: 650 }}> <Paper
<JudulList withBorder
title='List Program Kreatif Desa' bg={colors['white-1']}
href='/admin/inovasi/program-kreatif-desa/create' p="lg"
/> shadow="md"
radius="md"
h={{ base: 'auto', md: 650 }}
>
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Program Kreatif Desa</Title>
<Tooltip label="Tambah Program Kreatif Desa" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/inovasi/program-kreatif-desa/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowY: 'auto' }}> <Box style={{ overflowY: 'auto' }}>
<Table striped withTableBorder withRowBorders> <Table highlightOnHover striped >
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh> <TableTh style={{ width: '5%', textAlign: 'center' }}>No</TableTh>
<TableTh style={{ width: '20%' }}>Nama Program Kreatif Desa</TableTh> <TableTh style={{ width: '20%' }}>
<Text lineClamp={1} fw={"bold"} fz="sm">Nama Program Kreatif Desa</Text>
</TableTh>
<TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh> <TableTh style={{ width: '35%' }}>Deskripsi Singkat</TableTh>
<TableTh style={{ width: '10%' }}>Ikon</TableTh> <TableTh style={{ width: '10%', textAlign: 'center' }}>Ikon</TableTh>
<TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh> <TableTh style={{ width: '15%', textAlign: 'center' }}>Detail</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
@@ -132,9 +196,19 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
{filteredData.map((item, index) => ( {filteredData.map((item, index) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd> <TableTd style={{ width: '5%', textAlign: 'center' }}>{index + 1}</TableTd>
<TableTd style={{ width: '20%', wordWrap: 'break-word' }}>{item.name}</TableTd> <TableTd style={{ width: '20%', wordWrap: 'break-word' }}>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }} dangerouslySetInnerHTML={{ __html: item.slug }}></TableTd> <Box w={200}>
<TableTd style={{ width: '10%' }}> <Text fw={500} lineClamp={1}>{item.name}</Text>
</Box>
</TableTd>
<TableTd style={{ width: '35%', wordWrap: 'break-word' }}>
<Box w={150}>
<Text fz="sm" c="dimmed" lineClamp={1}>
{item.slug}
</Text>
</Box>
</TableTd>
<TableTd style={{ width: '10%', textAlign: 'center' }}>
{iconMap[item.icon] && ( {iconMap[item.icon] && (
<Box title={item.icon}> <Box title={item.icon}>
{React.createElement(iconMap[item.icon], { size: 24 })} {React.createElement(iconMap[item.icon], { size: 24 })}
@@ -142,8 +216,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
)} )}
</TableTd> </TableTd>
<TableTd style={{ width: '15%', textAlign: 'center' }}> <TableTd style={{ width: '15%', textAlign: 'center' }}>
<Button onClick={() => router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)}> <Button
<IconDeviceImac size={25} /> size="xs"
radius="md"
variant="light"
color="blue"
onClick={() =>
router.push(`/admin/inovasi/program-kreatif-desa/${item.id}`)
}
>
<IconDeviceImac size={18} />
<Text ml={6}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
@@ -157,14 +240,17 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
value={page} value={page}
onChange={(newPage) => { onChange={(newPage) => {
load(newPage, 10); load(newPage, 10);
window.scrollTo(0, 0); window.scrollTo({ top: 0, behavior: 'smooth' });
}} }}
total={totalPages} total={totalPages}
mt="md" mt="md"
mb="md" mb="md"
color="blue"
radius="md"
/> />
</Center> </Center>
</Box> </Box>
); );
} }
export default ProgramKreatifDesa; export default ProgramKreatifDesa;

View File

@@ -26,6 +26,7 @@ import { toast } from "react-toastify";
import { useProxy } from "valtio/utils"; import { useProxy } from "valtio/utils";
function EditKontakDaruratKeamanan() { function EditKontakDaruratKeamanan() {
const [isLoading, setIsLoading] = useState(true);
const router = useRouter(); const router = useRouter();
const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState); const kontakState = useProxy(kontakDarurat.kontakDaruratKeamananState);
const params = useParams(); const params = useParams();
@@ -37,29 +38,31 @@ function EditKontakDaruratKeamanan() {
// Load data // Load data
useEffect(() => { useEffect(() => {
kontakDarurat.kontakDaruratItem.findMany.load(); const loadData = async () => {
const loadKontakDarurat = async () => {
const id = params?.id as string;
if (!id) return;
try { try {
const data = await kontakState.update.load(id); setIsLoading(true);
if (data) { await kontakDarurat.kontakDaruratItem.findMany.load();
setFormData({ const id = params?.id as string;
name: data.nama || "", if (id) {
icon: data.icon || "", const data = await kontakState.update.load(id);
kategoriId: data.kategoriId || [], if (data) {
}); setFormData({
name: data.nama || "",
icon: data.icon || "",
kategoriId: data.kategoriId || [],
});
}
} }
} catch (error) { } catch (error) {
console.error("Error loading kontak darurat:", error); console.error("Error loading data:", error);
toast.error("Gagal memuat data kontak darurat"); toast.error("Gagal memuat data");
} finally {
setIsLoading(false);
} }
}; };
loadKontakDarurat(); loadData();
}, [params?.id]); }, [params?.id]);
// Handle submit // Handle submit
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
@@ -120,17 +123,20 @@ function EditKontakDaruratKeamanan() {
value={formData.kategoriId} value={formData.kategoriId}
onChange={(val) => setFormData({ ...formData, kategoriId: val })} onChange={(val) => setFormData({ ...formData, kategoriId: val })}
label={<Text fw={"bold"} fz={"sm"}>Kontak Item</Text>} label={<Text fw={"bold"} fz={"sm"}>Kontak Item</Text>}
placeholder='Pilih kontak item' placeholder={isLoading ? "Memuat data..." : "Pilih kontak item"}
data={ data={
kontakDarurat.kontakDaruratItem.findMany.data?.map((v) => ({ Array.isArray(kontakDarurat.kontakDaruratItem.findMany.data)
value: v.id, // Make sure this is using the ID ? kontakDarurat.kontakDaruratItem.findMany.data.map((v) => ({
label: v.nama value: v.id,
})) || [] label: v.nama
}))
: []
} }
clearable clearable
searchable searchable
required required
error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined} error={!formData.kategoriId.length ? "Pilih minimal satu kategori" : undefined}
disabled={isLoading}
/> />
<Box> <Box>

View File

@@ -148,12 +148,16 @@ function EditLaporanPublik() {
required required
/> />
<TextInput <Box>
value={formData.kronologi} <Text fw="bold" fz="sm" mb={6}>Kronologi Laporan Publik</Text>
onChange={(e) => setFormData({ ...formData, kronologi: e.target.value })} <EditEditor
label={<Text fw="bold" fz="sm">Kronologi Laporan Publik</Text>} value={formData.kronologi}
placeholder="Masukkan kronologi laporan publik" onChange={(htmlContent) => {
/> setFormData((prev) => ({ ...prev, kronologi: htmlContent }));
stateLaporan.edit.form.kronologi = htmlContent;
}}
/>
</Box>
<Box> <Box>
<Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text> <Text fw="bold" fz="sm" mb={6}>Penanganan Laporan Publik</Text>

View File

@@ -104,7 +104,9 @@ function ListProgramKesehatan({ search }: { search: string }) {
</Text> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> <Box w={200}>
<Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/> <Image w={100} src={item.image?.link} alt="image" radius="md" loading="lazy"/>

View File

@@ -1,19 +1,54 @@
/* 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";
async function ajukanIdeInovatifFindMany(context: Context) {
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 = [
{ name: { contains: search, mode: 'insensitive' } },
{ alamat: { contains: search, mode: 'insensitive' } },
];
}
export default async function ajukanIdeInovatifFindMany() {
try { try {
const data = await prisma.ajukanIdeInovatif.findMany({}); const [data, total] = await Promise.all([
prisma.ajukanIdeInovatif.findMany({
where: where, // Use the where clause that includes search conditions
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.ajukanIdeInovatif.count({
where: where // Also update the count query to use the same where clause
})
]);
return { return {
success: true, success: true,
message: "Success fetch ajukan ide inovatif", message: "Success fetch ajukan ide inovatif with pagination",
data, data,
page,
limit,
totalPages: Math.ceil(total / limit),
total,
}; };
} catch (error) { } catch (e) {
console.error("Find many error:", error); console.error("Find many paginated error:", e);
return { return {
success: false, success: false,
message: "Failed fetch ajukan ide inovatif", message: "Failed fetch ajukan ide inovatif with pagination",
}; };
} }
} }
export default ajukanIdeInovatifFindMany;

View File

@@ -23,16 +23,16 @@ async function administrasiOnlineFindMany(context: Context) {
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.administrasiOnline.findMany({ prisma.administrasiOnline.findMany({
where: { isActive: true }, where: where, // Use the where clause that includes search conditions
include: { include: {
jenisLayanan: true, jenisLayanan: true,
}, },
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { createdAt: 'desc' },
}), }),
prisma.administrasiOnline.count({ prisma.administrasiOnline.count({
where: { isActive: true } where: where // Also update the count query to use the same where clause
}) })
]); ]);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts // /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,12 +6,24 @@ import { Context } from "elysia";
async function pengaduanMasyarakatFindMany(context: Context) { async function pengaduanMasyarakatFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ judulPengaduan: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.pengaduanMasyarakat.findMany({ prisma.pengaduanMasyarakat.findMany({
where: { isActive: true }, where: where,
include: { include: {
jenisPengaduan: true, jenisPengaduan: true,
image: true, image: true,
@@ -20,7 +33,7 @@ async function pengaduanMasyarakatFindMany(context: Context) {
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
}), }),
prisma.pengaduanMasyarakat.count({ prisma.pengaduanMasyarakat.count({
where: { isActive: true } where: where
}) })
]); ]);
@@ -29,6 +42,7 @@ async function pengaduanMasyarakatFindMany(context: Context) {
message: "Success fetch pengaduan masyarakat with pagination", message: "Success fetch pengaduan masyarakat with pagination",
data, data,
page, page,
limit,
totalPages: Math.ceil(total / limit), totalPages: Math.ceil(total / limit),
total, total,
}; };

View File

@@ -1,15 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 jenisPengaduanFindMany() { export default async function jenisPengaduanFindMany(context: Context) {
const data = await prisma.jenisPengaduan.findMany(); // Ambil parameter dari query
return { 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 = [
{ nama: { contains: search, mode: 'insensitive' } }
];
}
try {
const [data, total] = await Promise.all([
prisma.jenisPengaduan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.jenisLayanan.count({ where }),
]);
return {
success: true, success: true,
data: data.map((item: any) => { data: data.map((item: any) => {
return { return {
id: item.id, id: item.id,
nama: item.nama, nama: item.nama
} }
}), }),
page,
limit,
total,
totalPages: Math.ceil(total / limit),
}; };
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data jenis pengaduan",
};
}
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia"; import { Context } from "elysia";
@@ -5,29 +6,39 @@ import { Context } from "elysia";
export default async function programKreatifFindMany(context: Context) { export default async function programKreatifFindMany(context: Context) {
const page = Number(context.query.page) || 1; const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 10; const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Buat where clause
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try { try {
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.programKreatif.findMany({ prisma.programKreatif.findMany({
where: { isActive: true }, where: where,
skip, skip,
take: limit, take: limit,
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}), }),
prisma.programKreatif.count({ prisma.programKreatif.count({
where: { isActive: true } where: where
}) })
]); ]);
const totalPages = Math.ceil(total / limit);
return { return {
success: true, success: true,
message: "Success fetch program kreatif with pagination", message: "Success fetch program kreatif with pagination",
data, data,
page, page,
totalPages, limit,
totalPages: Math.ceil(total / limit),
total, total,
}; };
} catch (e) { } catch (e) {
@@ -35,10 +46,6 @@ export default async function programKreatifFindMany(context: Context) {
return { return {
success: false, success: false,
message: "Failed fetch program kreatif with pagination", message: "Failed fetch program kreatif with pagination",
data: [],
page: 1,
totalPages: 1,
total: 0,
}; };
} }
} }

View File

@@ -0,0 +1,33 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormCreate = {
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
};
async function createPendaftaran(context: Context) {
const body = context.body as FormCreate;
await prisma.pendaftaranJadwalKegiatan.create({
data: {
name: body.name,
tanggal: body.tanggal,
namaOrangtua: body.namaOrangtua,
nomor: body.nomor,
alamat: body.alamat,
catatan: body.catatan,
},
});
return {
success: true,
message: "Success create pendaftaran jadwal kegiatan",
data: {
...body,
},
};
}
export default createPendaftaran;

View File

@@ -0,0 +1,23 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanDelete(context: Context) {
const id = context.params?.id as string;
if (!id) {
return {
status: 400,
body: "ID tidak diberikan",
};
}
await prisma.pendaftaranJadwalKegiatan.delete({
where: { id },
});
return {
success: true,
message: "Pendaftaran jadwal kegiatan berhasil dihapus",
status: 200,
};
}

View File

@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanFindMany(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 = [
{ name: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.pendaftaranJadwalKegiatan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.pendaftaranJadwalKegiatan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil pendaftaran jadwal kegiatan 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 pendaftaran jadwal kegiatan",
};
}
}

View File

@@ -0,0 +1,45 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function pendaftaranJadwalKegiatanFindUnique(context: Context) {
const id = context.params?.id as string;
if (!id) {
return Response.json({
success: false,
message: "ID tidak ditemukan",
}, {status: 400});
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, {status: 400});
}
const data = await prisma.pendaftaranJadwalKegiatan.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Pendaftaran jadwal kegiatan tidak ditemukan",
}, {status: 404});
}
return Response.json({
success: true,
message: "Success fetch pendaftaran jadwal kegiatan by ID",
data,
}, {status: 200});
} catch (error) {
console.error("Find by ID error:", error);
return Response.json({
success: false,
message: "Gagal mengambil pendaftaran jadwal kegiatan: " + (error instanceof Error ? error.message : 'Unknown error'),
}, {status: 500});
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import pendaftaranJadwalKegiatanCreate from "./create";
import pendaftaranJadwalKegiatanDelete from "./del";
import pendaftaranJadwalKegiatanUpdate from "./updt";
import pendaftaranJadwalKegiatanFindUnique from "./findUnique";
import pendaftaranJadwalKegiatanFindMany from "./findMany";
const PendaftaranJadwalKegiatan = new Elysia({
prefix: "/pendaftaran-jadwal-kegiatan",
tags: ["Kesehatan/Pendaftaran Jadwal Kegiatan"],
})
.post("/create", pendaftaranJadwalKegiatanCreate, {
body: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
})
.get("/find-many", pendaftaranJadwalKegiatanFindMany)
.get("/:id", pendaftaranJadwalKegiatanFindUnique)
.delete("/del/:id", pendaftaranJadwalKegiatanDelete)
.put(
"/:id",
pendaftaranJadwalKegiatanUpdate,
{
body: t.Object({
name: t.String(),
tanggal: t.String(),
namaOrangtua: t.String(),
nomor: t.String(),
alamat: t.String(),
catatan: t.String(),
}),
}
);
export default PendaftaranJadwalKegiatan;

View File

@@ -0,0 +1,87 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
type FormUpdate = {
id: string;
name: string;
tanggal: string;
namaOrangtua: string;
nomor: string;
alamat: string;
catatan: string;
}
export default async function mitraKolaborasiUpdate(context: Context) {
try {
const id = context.params?.id as string;
const body = (await context.body) as Omit<FormUpdate, "id">;
const {
tanggal,
namaOrangtua,
nomor,
alamat,
catatan
} = body;
if (!id) {
return new Response(JSON.stringify({
success: false,
message: "ID tidak ditemukan",
}), {
status: 400,
headers: {
'Content-Type': 'application/json'
}
})
}
const existing = await prisma.pendaftaranJadwalKegiatan.findUnique({
where: {id},
})
if (!existing) {
return new Response(JSON.stringify({
success: false,
message: "Pendaftaran jadwal kegiatan tidak ditemukan",
}), {
status: 404,
headers: {
'Content-Type': 'application/json'
}
})
}
const updated = await prisma.pendaftaranJadwalKegiatan.update({
where: { id },
data: {
tanggal,
namaOrangtua,
nomor,
alamat,
catatan,
}
})
return new Response(JSON.stringify({
success: true,
message: "Pendaftaran jadwal kegiatan berhasil diupdate",
data: updated,
}), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
} catch (error) {
console.error("Error updating pendaftaran jadwal kegiatan:", error);
return new Response(JSON.stringify({
success: false,
message: "Terjadi kesalahan saat mengupdate pendaftaran jadwal kegiatan",
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
})
}
}

View File

@@ -19,6 +19,7 @@ import ArtikelKesehatan from "./data_kesehatan_warga/artikel_kesehatan";
import Kelahiran from "./data_kesehatan_warga/persentase_kelahiran_kematian/kelahiran"; import Kelahiran from "./data_kesehatan_warga/persentase_kelahiran_kematian/kelahiran";
import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian"; import Kematian from "./data_kesehatan_warga/persentase_kelahiran_kematian/kematian";
import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis"; import DokterTenagaMedis from "./data_kesehatan_warga/fasilitas_kesehatan/dokter-tenaga-medis";
import PendaftaranJadwalKegiatan from "./data_kesehatan_warga/jadwal_kegiatan/pendaftaran";
const Kesehatan = new Elysia({ const Kesehatan = new Elysia({
@@ -45,4 +46,5 @@ const Kesehatan = new Elysia({
.use(Kelahiran) .use(Kelahiran)
.use(Kematian) .use(Kematian)
.use(DokterTenagaMedis) .use(DokterTenagaMedis)
.use(PendaftaranJadwalKegiatan)
export default Kesehatan; export default Kesehatan;

View File

@@ -1,58 +1,108 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Container, Text, Image, ListItem, List } from '@mantine/core'; import { Box, Center, Image, List, ListItem, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
import React from 'react';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
function Page() { const data1 = [
return ( {
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}> id: 1,
<Box px={{ base: "md", md: 100 }}><BackButton /></Box> judul: 'Peran Pecalang dalam Keamanan Desa',
<Container w={{ base: "100%", md: "50%" }} > image: '/api/img/pecalang.png',
<Box pb={20}> pengertian: 'Pecalang adalah petugas keamanan adat di Bali yang memiliki peran penting dalam menjaga ketertiban dan budaya lokal. Tugas mereka meliputi:',
<Text ta={"center"} fz={"3.4rem"} c={colors["blue-button"]} fw={"bold"}> deskripsi: <List>
Taman Beji Cengana <ListItem fz={{ base: 'h4', md: 'lg' }}>Mengamankan upacara adat dan kegiatan keagamaan.</ListItem>
</Text> <ListItem fz={{ base: 'h4', md: 'lg' }}>Mengatur lalu lintas saat ada perayaan atau kegiatan besar.</ListItem>
<Text <ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli untuk mencegah gangguan keamanan di lingkungan desa.</ListItem>
ta={"center"} <ListItem fz={{ base: 'h4', md: 'lg' }}>Berkoordinasi dengan aparat desa dan kepolisian dalam penanganan situasi darurat.</ListItem>
fw={"bold"} </List>
fz={"1.5rem"} },
> {
Informasi dan Pelayanan Administrasi Digital id: 2,
</Text> judul: 'Patwal (Patroli Pengawal) Desa',
</Box> image: '/api/img/patwal-1.png',
<Image src="/api/img/taman-beji.jpg" alt='' w={"100%"} h={400} /> pengertian: 'Selain Pecalang, Desa Darmasaba juga memiliki Patwal yang bertugas menjaga keamanan sehari-hari. Peran mereka antara lain:',
</Container> deskripsi: <List>
<Box px={{ base: "md", md: 100 }}> <ListItem fz={{ base: 'h4', md: 'lg' }}>Berpatroli secara rutin untuk memastikan lingkungan tetap aman.</ListItem>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"}> <ListItem fz={{ base: 'h4', md: 'lg' }}>Menjaga ketertiban lalu lintas di area desa.</ListItem>
Taman Beji Cengana, terletak di Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali, adalah situs suci yang memiliki nilai spiritual dan sejarah yang tinggi. Tempat ini dikenal sebagai lokasi untuk ritual pembersihan diri (melukat) dan peribadatan oleh umat Hindu Bali. Keberadaan mata air suci (Tirta Klebutan) di Taman Beji Cengana dipercaya memberikan berkah dan penyucian bagi mereka yang datang untuk berdoa dan melakukan ritual. <ListItem fz={{ base: 'h4', md: 'lg' }}>Melakukan tindakan preventif terhadap potensi gangguan keamanan.</ListItem>
</Text> <ListItem fz={{ base: 'h4', md: 'lg' }}>Siap siaga dalam keadaan darurat untuk membantu warga.</ListItem>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}> </List>
Potensi Desa melalui Taman Beji Cengana: },
</Text> {
<List py={20} type='ordered'> id: 3,
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}> judul: 'Layanan Keamanan yang Tersedia',
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengembangan Pariwisata Spiritual:</Text> Taman Beji Cengana memiliki potensi besar sebagai destinasi wisata spiritual. Wisatawan yang mencari pengalaman spiritual dan ketenangan batin dapat tertarik untuk mengunjungi tempat ini, mengikuti ritual melukat, dan merasakan suasana sakral yang ditawarkan. image: '/api/img/pospecalang.png',
</ListItem> pengertian: 'Jika terjadi keadaan darurat atau membutuhkan bantuan keamanan, warga dapat menghubungi:',
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}> deskripsi: <List>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pelestarian Budaya dan Tradisi:</Text>Dengan mempromosikan Taman Beji Cengana sebagai pusat kegiatan budaya dan ritual tradisional, desa dapat memastikan bahwa warisan budaya dan tradisi lokal tetap lestari. <ListItem fz={{ base: 'h4', md: 'lg' }}>Pos Pecalang Desa: [Masukkan Nomor Kontak].</ListItem>
</ListItem> <ListItem fz={{ base: 'h4', md: 'lg' }}>Patwal Desa Darmasaba: [Masukkan Nomor Kontak].</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}> <ListItem fz={{ base: 'h4', md: 'lg' }}>Polsek Terdekat: 110 (Layanan Kepolisian).</ListItem>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pendidikan dan Penelitian:</Text>Taman Beji Cengana dapat dijadikan sebagai pusat pendidikan dan penelitian bagi akademisi, peneliti, dan pelajar yang tertarik mempelajari budaya, agama, dan sejarah Bali. </List>
</ListItem> },
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}> {
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengembangan Ekonomi Kreatif:</Text>Dengan meningkatnya jumlah pengunjung ke Taman Beji Cengana, peluang bagi pengembangan ekonomi kreatif juga terbuka lebar. Masyarakat lokal dapat mengembangkan produk kerajinan tangan, kuliner khas, dan suvenir yang mencerminkan budaya dan tradisi desa. id: 4,
</ListItem> judul: 'Program Keamanan Desa',
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}> image: '/api/img/rond.png',
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Konservasi Lingkungan:</Text>Sebagai situs suci dengan mata air alami, Taman Beji Cengana memiliki peran penting dalam konservasi lingkungan. Upaya menjaga kebersihan dan kelestarian mata air serta lingkungan sekitarnya dapat menjadi contoh praktik konservasi yang baik. pengertian: 'Untuk meningkatkan keamanan, Desa Darmasaba menjalankan berbagai program, seperti:',
</ListItem> deskripsi: <List>
</List> <ListItem fz={{ base: 'h4', md: 'lg' }}>Ronda Malam Warga: Kegiatan jaga malam secara bergilir oleh warga bersama Pecalang dan Patwal.</ListItem>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}> <ListItem fz={{ base: 'h4', md: 'lg' }}>Sosialisasi Keamanan: Edukasi bagi warga tentang cara menjaga keamanan lingkungan.</ListItem>
Dengan memanfaatkan potensi yang dimiliki Taman Beji Cengana, Desa Darmasaba dapat mengembangkan sektor pariwisata, budaya, pendidikan, ekonomi, dan lingkungan secara berkelanjutan, yang pada gilirannya akan meningkatkan kesejahteraan masyarakat dan pelestarian warisan budaya. <ListItem fz={{ base: 'h4', md: 'lg' }}> Pengawasan Kamera CCTV: Memantau titik- titik strategis untuk mencegah tindak kejahatan.</ListItem>
</Text> </List>
</Box>
</Stack>
);
} }
]
function Page() {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Keamanan Lingkungan (Pecalang / Patwal)
</Text>
<Text px={{ base: 20, md: 150 }} ta={"center"} fz={{ base: "h4", md: "h3" }} >
Keamanan dan ketertiban lingkungan di Desa Darmasaba dijaga melalui peran aktif Pecalang dan Patwal (Patroli Pengawal). Mereka bertugas memastikan desa tetap aman, tertib, dan kondusif bagi seluruh warga.
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}>
<SimpleGrid
pb={10}
cols={{
base: 1,
md: 3,
}}>
{data1.map((v, k) => {
return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}>
<Center px={10} py={20}>
<Image src={v.image} alt='' />
</Center>
<Box px={'lg'}>
<Box pb={20}>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul}
</Text>
<Text pb={10} fz={"h4"} ta={'justify'}>
{v.pengertian}
</Text>
<Box px={10}>
{v.deskripsi}
</Box>
</Box>
</Box>
</Stack>
</Paper>
)
})}
</SimpleGrid>
</Stack>
</Box>
</Stack>
);
}
export default Page; export default Page;

View File

@@ -120,7 +120,7 @@ function Page() {
<Box> <Box>
<Text fw={600} fz="lg" >Kronologi</Text> <Text fw={600} fz="lg" >Kronologi</Text>
<Text fz="sm" c="dimmed">{data.kronologi || '-'}</Text> <Text fz="sm" c="dimmed" dangerouslySetInnerHTML={{ __html: data.kronologi || '-' }} />
</Box> </Box>
<Divider /> <Divider />

View File

@@ -55,27 +55,28 @@ function Page() {
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<BackButton /> <BackButton />
<Flex gap={"xs"} align={"center"}> <TextInput
<TextInput placeholder="Cari laporan"
placeholder="Cari laporan" value={search}
value={search} onChange={(e) => setSearch(e.currentTarget.value)}
onChange={(e) => setSearch(e.currentTarget.value)} />
/>
<Button
onClick={open}
bg={colors['blue-button']}
size="md"
radius="md"
>
<IconPlus size={20} />
</Button>
</Flex>
</Flex> </Flex>
</Box> </Box>
<Box> <Box px={{ base: 'md', md: 100 }}>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Group justify="space-between">
Laporan Keamanan Lingkungan <Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
</Text> Laporan Keamanan Lingkungan
</Text>
<Button
onClick={open}
bg={colors['blue-button']}
size="md"
radius="md"
rightSection={<IconPlus size={20} />}
>
Tambah Laporan
</Button>
</Group>
</Box> </Box>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Stack gap={'lg'}>

View File

@@ -1,11 +1,129 @@
import React from 'react'; 'use client'
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { Box, Button, Card, Center, Group, Loader, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowLeft } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
function DetailPencegahanKriminalitas() {
const router = useRouter();
const params = useParams();
const kriminalitasState = useProxy(pencegahanKriminalitasState);
useShallowEffect(() => {
kriminalitasState.findUnique.load(params?.id as string);
}, []);
if (kriminalitasState.findUnique.loading) {
return (
<Center py="xl">
<Loader size="lg" color="blue" />
</Center>
);
}
if (!kriminalitasState.findUnique.data) {
return (
<Center h={400}>
<Stack align="center" gap="sm">
<Text fz="lg" fw="bold" c="dimmed">Data tidak ditemukan</Text>
<Button
variant="light"
color="blue"
leftSection={<IconArrowLeft size={18} />}
onClick={() => router.push("/admin/keamanan/pencegahan-kriminalitas")}
>
Kembali ke daftar
</Button>
</Stack>
</Center>
);
}
const data = kriminalitasState.findUnique.data;
function Page() {
return ( return (
<div> <Box py="md" px="md">
Page <Group mb="md">
</div> <Button
variant="light"
color="blue"
onClick={() => router.back()}
leftSection={<IconArrowLeft size={20} />}
>
Kembali
</Button>
</Group>
<Card
withBorder
radius="xl"
shadow="md"
p="xl"
bg="white"
>
<Stack gap="lg">
<Title order={2} c="blue">Detail Pencegahan Kriminalitas</Title>
<Paper radius="lg" p="lg" withBorder>
<Stack gap="lg">
<Stack gap={4}>
<Text fz="sm" c="dimmed">Judul</Text>
<Text fz="lg" fw={600}>{data?.judul || '-'}</Text>
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Deskripsi Singkat</Text>
{data?.deskripsiSingkat ? (
<Text fz="md" dangerouslySetInnerHTML={{ __html: data.deskripsiSingkat }} />
) : (
<Text fz="sm" c="dimmed">Belum ada deskripsi singkat</Text>
)}
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Deskripsi Lengkap</Text>
{data?.deskripsi ? (
<Text fz="md" dangerouslySetInnerHTML={{ __html: data.deskripsi }} />
) : (
<Text fz="sm" c="dimmed">Belum ada deskripsi</Text>
)}
</Stack>
<Stack gap={4}>
<Text fz="sm" c="dimmed">Video</Text>
{data?.linkVideo ? (
<Box
component="iframe"
src={convertToEmbedUrl(data.linkVideo)}
width="100%"
h={{ base: 320, md: 450 }}
allowFullScreen
style={{ borderRadius: 12, border: 'none' }}
/>
) : (
<Text fz="sm" c="dimmed">Belum ada video</Text>
)}
</Stack>
</Stack>
</Paper>
</Stack>
</Card>
</Box>
); );
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch {
return youtubeUrl;
}
}
} }
export default Page; export default DetailPencegahanKriminalitas;

View File

@@ -1,11 +1,137 @@
import React from 'react'; 'use client'
import {
Box,
Card,
Center,
Group,
Pagination,
Skeleton,
Stack,
Text,
Title,
Tooltip,
Button,
Paper,
} from '@mantine/core';
import { IconSearch, IconArrowRight } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import pencegahanKriminalitasState from '@/app/admin/(dashboard)/_state/keamanan/pencegahan-kriminalitas';
import { useShallowEffect } from '@mantine/hooks';
import { useState } from 'react';
import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
function PencegahanKriminalitas() {
const [search, setSearch] = useState("");
function Page() {
return ( return (
<div> <Box>
Page <HeaderSearch
</div> title="Program Pencegahan Kriminalitas"
placeholder="Cari program atau deskripsi..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPencegahanKriminalitas search={search} />
</Box>
); );
} }
export default Page; function ListPencegahanKriminalitas({ search }: { search: string }) {
const kriminalitasState = useProxy(pencegahanKriminalitasState);
const router = useRouter();
const { data, page, totalPages, loading, load } = kriminalitasState.findMany;
useShallowEffect(() => {
load(page, 6, search);
}, [page, search]);
if (loading || !data) {
return (
<Stack py="lg">
<Skeleton height={300} radius="lg" />
<Skeleton height={300} radius="lg" />
</Stack>
);
}
return (
<Box py="lg">
<Stack>
{data.length > 0 ? (
data.map((item) => (
<Card
key={item.id}
withBorder
radius="lg"
shadow="md"
p="lg"
style={{
background: 'linear-gradient(135deg, #ffffff 0%, #f9fbff 100%)',
}}
>
<Stack gap="xs">
<Title order={4} c="blue" style={{ fontWeight: 600 }}>
{item.judul}
</Title>
<Text
fz="sm"
c="dimmed"
lineClamp={2}
dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat || '' }}
/>
<Group justify="flex-end" mt="sm">
<Tooltip label="Lihat detail program" withArrow>
<Button
size="sm"
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
rightSection={<IconArrowRight size={18} />}
onClick={() => router.push(`/darmasaba/keamanan/pencegahan-kriminalitas/${item.id}`)}
>
Lihat Detail
</Button>
</Tooltip>
</Group>
</Stack>
</Card>
))
) : (
<Paper withBorder radius="lg" p="xl" shadow="sm">
<Center>
<Stack align="center" gap="xs">
<Text fz="lg" fw={500} c="dimmed">
Belum ada program pencegahan kriminalitas
</Text>
<Text fz="sm" c="dimmed">
Program akan ditampilkan di sini ketika tersedia
</Text>
</Stack>
</Center>
</Paper>
)}
</Stack>
{totalPages > 1 && (
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 6, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="xl"
color="blue"
radius="lg"
/>
</Center>
)}
</Box>
);
}
export default PencegahanKriminalitas;

View File

@@ -3,55 +3,23 @@ import jadwalkegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_k
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto'; import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
ActionIcon,
Box, Box,
Button, Button,
Combobox,
ComboboxChevron,
ComboboxOption,
ComboboxOptions,
ComboboxTarget,
Divider, Divider,
Group, Group,
InputBase,
InputPlaceholder,
Paper, Paper,
Skeleton, Skeleton,
Stack, Stack,
Text, Text
Textarea,
TextInput,
useCombobox
} from '@mantine/core'; } from '@mantine/core';
import { DateInput } from '@mantine/dates';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconDownload, IconPhone, IconMail, IconUser } from '@tabler/icons-react'; import { IconDownload, IconMail, IconPhone, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import CreatePendaftaran from '../create/page';
const layanan = [
'Penimbangan',
'Imunisasi',
'Vitamin A',
'Konsultasi Gizi',
'Pemeriksaan Kesehatan'
];
function Page() { function Page() {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: (eventSource) => {
if (eventSource === 'keyboard') {
combobox.selectActiveOption();
} else {
combobox.updateSelectedOptionIndex('active');
}
},
});
const [value, setValue] = useState<string | null>('');
const [dateInputOpened, setDateInputOpened] = useState(false);
const params = useParams(); const params = useParams();
const state = useProxy(jadwalkegiatanState); const state = useProxy(jadwalkegiatanState);
@@ -59,20 +27,7 @@ function Page() {
state.findUnique.load(params?.id as string); state.findUnique.load(params?.id as string);
}, []); }, []);
const options = layanan.map((item) => (
<ComboboxOption value={item} key={item} active={item === value}>
<Group gap="xs">
{item === value && <IconUser size={14} stroke={2} />}
<span>{item}</span>
</Group>
</ComboboxOption>
));
const pickerControl = (
<ActionIcon onClick={() => setDateInputOpened(true)} variant="light" color="blue">
<IconCalendar size={18} />
</ActionIcon>
);
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
@@ -133,56 +88,7 @@ function Page() {
<Text ta="justify" fz="md" dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} /> <Text ta="justify" fz="md" dangerouslySetInnerHTML={{ __html: state.findUnique.data.dokumenjadwalkegiatan.content }} />
</Stack> </Stack>
<Stack gap="sm"> <CreatePendaftaran />
<Text fz="lg" fw="bold">Formulir Pendaftaran</Text>
<Divider />
<Stack gap="md">
<TextInput label="Nama Balita" placeholder="Masukkan nama balita" size="md" />
<DateInput
label="Tanggal Lahir"
placeholder="dd/mm/yyyy"
size="md"
w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}
popoverProps={{ opened: dateInputOpened, onChange: setDateInputOpened }}
rightSection={pickerControl}
/>
<TextInput label="Nama Orang Tua / Wali" placeholder="Masukkan nama orang tua / wali" size="md" />
<TextInput label="Nomor Telepon" placeholder="Masukkan nomor telepon" size="md" />
<TextInput label="Alamat" placeholder="Masukkan alamat lengkap" size="md" />
<Box w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}>
<Text fz="sm" fw="bold" pb={4}>Pilih Layanan</Text>
<Combobox
store={combobox}
resetSelectionOnOptionHover
withinPortal={false}
onOptionSubmit={(val) => {
setValue(val);
combobox.updateSelectedOptionIndex('active');
}}
>
<ComboboxTarget targetType="button">
<InputBase
component="button"
type="button"
pointer
rightSection={<ComboboxChevron />}
rightSectionPointerEvents="none"
onClick={() => combobox.toggleDropdown()}
>
{value || <InputPlaceholder>Pilih layanan</InputPlaceholder>}
</InputBase>
</ComboboxTarget>
<Combobox.Dropdown>
<ComboboxOptions>{options}</ComboboxOptions>
</Combobox.Dropdown>
</Combobox>
</Box>
<Textarea label="Catatan Khusus (Opsional)" placeholder="Masukkan catatan jika ada" size="md" />
<Button size="md" radius="lg" bg={colors['blue-button']}>
Daftar Sekarang
</Button>
</Stack>
</Stack>
<Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm"> <Paper p="lg" radius="md" bg={colors['blue-button-trans']} shadow="sm">
<Stack gap="xs"> <Stack gap="xs">

View File

@@ -0,0 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import pendaftaranJadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/pendafataranJadwalKegiatan';
import colors from '@/con/colors';
import { Button, Divider, Stack, Text, Textarea, TextInput } from '@mantine/core';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
function CreatePendaftaran() {
const stateCreate = useProxy(pendaftaranJadwalKegiatanState);
useEffect(() => {
stateCreate.findMany.load();
}, []);
const resetForm = () => {
stateCreate.create.form = {
name: '',
alamat: '',
tanggal: '',
namaOrangtua: '',
nomor: '',
catatan: '',
};
};
const handleSubmit = async () => {
await stateCreate.create.submit();
resetForm();
};
return (
<Stack gap="sm">
<Text fz="lg" fw="bold">Formulir Pendaftaran</Text>
<Divider />
<Stack gap="md">
<TextInput
label="Nama Balita"
placeholder="Masukkan nama balita"
size="md"
value={stateCreate.create.form.name}
onChange={(e) => stateCreate.create.form.name = e.target.value}
/>
<TextInput
type='date'
label="Tanggal Lahir"
placeholder="dd/mm/yyyy"
size="md"
w={{ base: '100%', md: '85%', lg: '75%', xl: '50%' }}
value={stateCreate.create.form.tanggal}
onChange={(e) => stateCreate.create.form.tanggal = e.target.value}
/>
<TextInput
label="Nama Orang Tua / Wali"
placeholder="Masukkan nama orang tua / wali"
size="md"
value={stateCreate.create.form.namaOrangtua}
onChange={(e) => stateCreate.create.form.namaOrangtua = e.target.value}
/>
<TextInput
label="Nomor Telepon"
placeholder="Masukkan nomor telepon"
size="md"
value={stateCreate.create.form.nomor}
onChange={(e) => stateCreate.create.form.nomor = e.target.value}
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat lengkap"
size="md"
value={stateCreate.create.form.alamat}
onChange={(e) => stateCreate.create.form.alamat = e.target.value}
/>
<Textarea
label="Catatan Khusus (Opsional)"
placeholder="Masukkan catatan jika ada"
size="md"
value={stateCreate.create.form.catatan}
onChange={(e) => stateCreate.create.form.catatan = e.target.value}
/>
<Button size="md" radius="lg" bg={colors['blue-button']} onClick={handleSubmit}>
Daftar Sekarang
</Button>
</Stack>
</Stack>
);
}
export default CreatePendaftaran;