Fix UI Admin Menu Kesehatan, Login Admin, OTP

This commit is contained in:
2025-09-08 14:02:21 +08:00
parent 8817b937b1
commit 797713ef49
80 changed files with 7648 additions and 4924 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -75,6 +75,7 @@
"prisma": "^6.3.1", "prisma": "^6.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-international-phone": "^4.6.0",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-simple-toasts": "^6.1.0", "react-simple-toasts": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",

View File

@@ -115,27 +115,38 @@ const artikelKesehatanState = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
artikelKesehatanState.findMany.loading = true; // ✅ Akses langsung via nama path
artikelKesehatanState.findMany.page = page;
artikelKesehatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)["artikel-kesehatan"][ if (search) query.search = search;
const res = await ApiFetch.api.kesehatan["artikel-kesehatan"][
"find-many" "find-many"
].get(); ].get({ query });
if (res.status === 200) { if (res.status === 200 && res.data?.success) {
this.data = res.data?.data ?? []; artikelKesehatanState.findMany.data =
res.data.data ?? [];
artikelKesehatanState.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data artikel kesehatan"); artikelKesehatanState.findMany.data = [];
artikelKesehatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch artikel kesehatan paginated:", err);
console.error("LOAD ERROR:", err); artikelKesehatanState.findMany.data = [];
throw err; artikelKesehatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; artikelKesehatanState.findMany.loading = false;
} }
}, },
}, },
@@ -280,12 +291,9 @@ const artikelKesehatanState = proxy({
async byId(id: string) { async byId(id: string) {
try { try {
artikelKesehatanState.delete.loading = true; artikelKesehatanState.delete.loading = true;
const res = await fetch( const res = await fetch(`/api/kesehatan/artikel-kesehatan/del/${id}`, {
`/api/kesehatan/artikel-kesehatan/del/${id}`, method: "DELETE",
{ });
method: "DELETE",
}
);
const result = await res.json(); const result = await res.json();
if (res.ok && result.success) { if (res.ok && result.success) {

View File

@@ -116,27 +116,38 @@ const fasilitasKesehatan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
fasilitasKesehatanState.fasilitasKesehatan.findMany.page = page;
fasilitasKesehatanState.fasilitasKesehatan.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"fasilitas-kesehatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["fasilitas-kesehatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
fasilitasKesehatanState.fasilitasKesehatan.findMany.data =
res.data.data ?? [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages =
res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data fasilitas kesehatan"); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch fasilitas kesehatan paginated:", err);
console.error("LOAD ERROR:", err); fasilitasKesehatanState.fasilitasKesehatan.findMany.data = [];
throw err; fasilitasKesehatanState.fasilitasKesehatan.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; fasilitasKesehatanState.fasilitasKesehatan.findMany.loading = false;
} }
}, },
}, },
@@ -558,7 +569,7 @@ const dokter = proxy({
const fasilitasKesehatanState = proxy({ const fasilitasKesehatanState = proxy({
fasilitasKesehatan, fasilitasKesehatan,
dokter dokter,
}); });
export default fasilitasKesehatanState; export default fasilitasKesehatanState;

View File

@@ -120,27 +120,36 @@ const jadwalkegiatanState = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
load: async (page = 1, limit = 10, search = "") => {
jadwalkegiatanState.findMany.loading = true; // ✅ Akses langsung via nama path
jadwalkegiatanState.findMany.page = page;
jadwalkegiatanState.findMany.search = search;
try { try {
this.loading = true; const query: any = { page, limit };
const res = await (ApiFetch.api.kesehatan as any)[ if (search) query.search = search;
"jadwal-kegiatan"
]["find-many"].get();
if (res.status === 200) { const res = await ApiFetch.api.kesehatan["jadwal-kegiatan"][
this.data = res.data?.data ?? []; "find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
jadwalkegiatanState.findMany.data = res.data.data ?? [];
jadwalkegiatanState.findMany.totalPages = res.data.totalPages ?? 1;
} else { } else {
toast.error("Gagal memuat data jadwal kegiatan"); jadwalkegiatanState.findMany.data = [];
jadwalkegiatanState.findMany.totalPages = 1;
} }
return res;
} catch (err) { } catch (err) {
toast.error("Terjadi error saat load data"); console.error("Gagal fetch jadwal kegiatan paginated:", err);
console.error("LOAD ERROR:", err); jadwalkegiatanState.findMany.data = [];
throw err; jadwalkegiatanState.findMany.totalPages = 1;
} finally { } finally {
this.loading = false; jadwalkegiatanState.findMany.loading = false;
} }
}, },
}, },
@@ -227,29 +236,42 @@ const jadwalkegiatanState = proxy({
content: jadwalkegiatanState.edit.form.content, content: jadwalkegiatanState.edit.form.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal, tanggal:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.tanggal,
waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu, waktu: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.waktu,
lokasi: jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi, lokasi:
jadwalkegiatanState.edit.form.informasiJadwalKegiatan.lokasi,
}, },
layananJadwalKegiatan: { layananJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.layananJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.layananJadwalKegiatan.content,
}, },
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: {
deskripsi: jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi, deskripsi:
jadwalkegiatanState.edit.form.deskripsiJadwalKegiatan.deskripsi,
}, },
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.syaratKetentuanJadwalKegiatan
.content,
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content, content:
jadwalkegiatanState.edit.form.dokumenJadwalKegiatan.content,
}, },
pendaftaranJadwalKegiatan: { pendaftaranJadwalKegiatan: {
name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name, name: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.name,
tanggal: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal, tanggal:
namaOrangtua: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.namaOrangtua, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.tanggal,
nomor: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor, namaOrangtua:
alamat: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat, jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan
catatan: jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan, .namaOrangtua,
nomor:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.nomor,
alamat:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.alamat,
catatan:
jadwalkegiatanState.edit.form.pendaftaranJadwalKegiatan.catatan,
}, },
}; };
@@ -286,7 +308,7 @@ const jadwalkegiatanState = proxy({
}, },
delete: { delete: {
loading: false, loading: false,
async byId(id: string){ async byId(id: string) {
try { try {
jadwalkegiatanState.delete.loading = true; jadwalkegiatanState.delete.loading = true;
const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, { const res = await fetch(`/api/kesehatan/jadwal-kegiatan/del/${id}`, {
@@ -305,7 +327,7 @@ const jadwalkegiatanState = proxy({
} finally { } finally {
jadwalkegiatanState.delete.loading = false; jadwalkegiatanState.delete.loading = false;
} }
} },
}, },
}); });

View File

@@ -90,6 +90,32 @@ const userState = proxy({
} }
}, },
}, },
updateActive: {
loading: false,
async submit(id: string, isActive: boolean) {
this.loading = true;
try {
const res = await fetch(`/api/user/updt`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, isActive }),
});
const data = await res.json();
if (res.status === 200 && data.success) {
toast.success(data.message);
userState.findMany.load(userState.findMany.page, 10, userState.findMany.search);
} else {
toast.error(data.message || "Gagal update status user");
}
} catch (e) {
console.error(e);
toast.error("Gagal update status user");
} finally {
this.loading = false;
}
},
},
}); });
const templateRole = z.object({ const templateRole = z.object({

View File

@@ -0,0 +1,111 @@
'use client'
import { apiFetchLogin } from '@/app/admin/auth/_lib/api_fetch_auth';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Image, Paper, Stack, Text, Title } from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Login() {
const router = useRouter()
const [phone, setPhone] = useState("")
const [isError, setError] = useState(false)
const [loading, setLoading] = useState(false)
async function onLogin() {
const nomor = phone.substring(1);
if (nomor.length <= 4) return setError(true)
try {
setLoading(true);
const response = await apiFetchLogin({ nomor: nomor })
if (response && response.success) {
localStorage.setItem("hipmi_auth_code_id", response.kodeId);
toast.success(response.message);
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
toast.error(response?.message);
}
} catch (error) {
setLoading(false)
console.log("Error Login", error)
toast.error("Terjadi kesalahan saat login")
}
}
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
</Box>
<Box>
{/* <Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masuk Untuk Akses Admin</Text>
<TextInput
label='Username'
placeholder='Username'
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</Box> */}
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%"}}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
{isError ? (
toast.error("Masukan nomor telepon anda")
) : (
""
)}
<Box py={20} >
<Button
fullWidth
bg={colors['blue-button']}
radius={'xl'}
onClick={onLogin}
loading={loading ? true : false}
>Masuk
</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Login;

View File

@@ -0,0 +1,121 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
'use client'
import { apiFetchRegister } from '@/app/admin/auth/_lib/api_fetch_auth';
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Button, Center, Checkbox, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { PhoneInput } from "react-international-phone";
import "react-international-phone/style.css";
import { toast } from 'react-toastify';
function Registrasi() {
const [phone, setPhone] = useState("")
const router = useRouter()
const [value, setValue] = useState("")
const [isValue, setIsValue] = useState(false);
const [loading, setLoading] = useState(false);
async function onRegistarsi() {
if (value.length < 5) {
toast.error("Username minimal 5 karakter!");
return;
}
if (value.includes(" ")) {
toast.error("Username tidak boleh ada spasi!");
return;
}
if (!phone) {
toast.error("Nomor telepon wajib diisi!");
return;
}
try {
setLoading(true);
const respone = await apiFetchRegister({ nomor: phone, username: value });
if (respone.success) {
router.push("/login", { scroll: false });
toast.success(respone.message);
} else {
setLoading(false);
toast.error(respone.message);
}
} catch (error) {
setLoading(false);
console.log("Error Registrasi", error);
}
}
return (
<Stack pos={"relative"} bg={colors.Bg} gap={"22"} py={"xl"} h={"100vh"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Stack justify='center' align='center' h={"80vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Registrasi
</Title>
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<TextInput placeholder='Username'
label='Username'
maxLength={50}
error={
value.length > 0 && value.length < 5
? "Minimal 5 karakter !"
: value.includes(" ")
? "Tidak boleh ada spasi"
: isValue
? "Masukan username anda"
: ""
}
onChange={(val) => {
val.currentTarget.value.length > 0 ? setIsValue(false) : "";
setValue(val.currentTarget.value);
}}
required
/>
<Box py={10}>
<Text fz={"sm"} >Nomor Telepon</Text>
<PhoneInput
countrySelectorStyleProps={{
buttonStyle: {
backgroundColor: colors['blue-button'],
},
}}
inputStyle={{ width: "100%" }}
defaultCountry="id"
onChange={(val) => {
setPhone(val);
}}
/>
</Box>
<Box pb={10}>
<Checkbox
label="Saya menyetujui syarat dan ketentuan yang berlaku"
/>
</Box>
<Box pb={20} >
<Button fullWidth bg={colors['blue-button']} radius={'xl'} onClick={onRegistarsi} loading={loading ? true : false}>Daftar</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Registrasi;

View File

@@ -0,0 +1,38 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, PinInput, Stack, Text, Title } from '@mantine/core';
import { useRouter } from 'next/navigation';
function Validasi() {
const router = useRouter()
return (
<Stack pos={"relative"} bg={colors.Bg}>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Stack align='center' justify='center' h={"100vh"}>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center' gap={"lg"}>
<Box>
<Title ta={"center"} order={2} fw={'bold'} c={colors['blue-button']}>
Kode Verifikasi
</Title>
</Box>
<Box>
<Box mb={10}>
<Text c={colors['blue-button']} ta={"center"} fz={"sm"} fw={'bold'}>Masukkan Kode Verifikasi</Text>
<PinInput type={/^[0-9]*$/} inputType="tel" inputMode="numeric" />
</Box>
<Box py={20} >
<Button onClick={() => router.push("/admin/landing-page/profile/program-inovasi")}>
Page
</Button>
</Box>
</Box>
</Stack>
</Paper>
</Stack>
</Box>
</Stack>
);
}
export default Validasi;

View File

@@ -1,77 +1,140 @@
/* 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 { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title, Tooltip } from '@mantine/core';
import { IconActivity, IconBuildingHospital, IconCalendarEvent, IconGauge, IconNotes } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "Presentase Kelahiran & Kematian",
value: "presentasekelahiran&kematian",
href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian"
},
{
label: "Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik",
value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"
},
{
label: "Fasilitas Kesehatan",
value: "fasilitaskesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan"
},
{
label: "Jadwal Kegiatan",
value: "jadwalkegiatan",
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan"
},
{
label: "Artikel Kesehatan",
value: "artikelkesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => { const tabs = [
const match = tabs.find(tab => tab.href === pathname) {
if (match) { label: "Presentase Kelahiran & Kematian",
setActiveTab(match.value) value: "presentasekelahiran&kematian",
} href: "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian",
}, [pathname]) icon: <IconActivity size={18} stroke={1.8} />,
tooltip: "Lihat data kelahiran dan kematian"
},
{
label: "Grafik Hasil Kepuasan Masyarakat",
value: "grafikhasilkepuasan",
href: "/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan",
icon: <IconGauge size={18} stroke={1.8} />,
tooltip: "Grafik kepuasan masyarakat terhadap pelayanan"
},
{
label: "Fasilitas Kesehatan",
value: "fasilitaskesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan",
icon: <IconBuildingHospital size={18} stroke={1.8} />,
tooltip: "Data fasilitas kesehatan desa"
},
{
label: "Jadwal Kegiatan",
value: "jadwalkegiatan",
href: "/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan",
icon: <IconCalendarEvent size={18} stroke={1.8} />,
tooltip: "Atur jadwal kegiatan kesehatan"
},
{
label: "Artikel Kesehatan",
value: "artikelkesehatan",
href: "/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan",
icon: <IconNotes size={18} stroke={1.8} />,
tooltip: "Artikel & informasi seputar kesehatan"
},
];
return (
<Stack> const currentTab = tabs.find(tab => tab.href === pathname);
<Title order={3}>Data Kesehatan Warga</Title> const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => ( const handleTabChange = (value: string | null) => {
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> const tab = tabs.find(t => t.value === value);
))} if (tab) {
</TabsList> router.push(tab.href);
{tabs.map((e, i) => ( }
<TabsPanel key={i} value={e.value}> setActiveTab(value);
{/* Konten dummy, bisa diganti tergantung routing */} };
<></>
</TabsPanel>
))} useEffect(() => {
</Tabs> const match = tabs.find(tab => tab.href === pathname);
{children} if (match) {
</Stack> setActiveTab(match.value);
); }
}, [pathname]);
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Data Kesehatan Warga
</Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
}}
>
{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>
{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}
</TabsPanel>
))}
</Tabs>
</Stack>
);
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
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';
@@ -14,29 +24,12 @@ import { useProxy } from 'valtio/utils';
interface ArtikelKesehatanFormBase { interface ArtikelKesehatanFormBase {
title: string; title: string;
content: string; content: string;
introduction: { introduction: { content: string };
content: string; symptom: { title: string; content: string };
}; prevention: { title: string; content: string };
symptom: { firstAid: { title: string; content: string };
title: string; mythVsFact: { title: string; mitos: string; fakta: string };
content: string; doctorSign: { content: string };
};
prevention: {
title: string;
content: string;
};
firstAid: {
title: string;
content: string;
};
mythVsFact: {
title: string;
mitos: string;
fakta: string;
};
doctorSign: {
content: string;
};
} }
function EditArtikelKesehatan() { function EditArtikelKesehatan() {
@@ -47,29 +40,27 @@ function EditArtikelKesehatan() {
const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({ const [formData, setFormData] = useState<ArtikelKesehatanFormBase>({
title: stateArtikelKesehatan.edit.form.title || '', title: stateArtikelKesehatan.edit.form.title || '',
content: stateArtikelKesehatan.edit.form.content || '', content: stateArtikelKesehatan.edit.form.content || '',
introduction: { introduction: { content: stateArtikelKesehatan.edit.form.introduction?.content || '' },
content: stateArtikelKesehatan.edit.form.introduction?.content || '',
},
symptom: { symptom: {
title: stateArtikelKesehatan.edit.form.symptom?.title || '', title: stateArtikelKesehatan.edit.form.symptom?.title || '',
content: stateArtikelKesehatan.edit.form.symptom?.content || '', content: stateArtikelKesehatan.edit.form.symptom?.content || ''
}, },
prevention: { prevention: {
title: stateArtikelKesehatan.edit.form.prevention?.title || '', title: stateArtikelKesehatan.edit.form.prevention?.title || '',
content: stateArtikelKesehatan.edit.form.prevention?.content || '', content: stateArtikelKesehatan.edit.form.prevention?.content || ''
}, },
firstAid: { firstAid: {
title: stateArtikelKesehatan.edit.form.firstAid?.title || '', title: stateArtikelKesehatan.edit.form.firstAid?.title || '',
content: stateArtikelKesehatan.edit.form.firstAid?.content || '', content: stateArtikelKesehatan.edit.form.firstAid?.content || ''
}, },
mythVsFact: { mythVsFact: {
title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '', title: stateArtikelKesehatan.edit.form.mythVsFact?.title || '',
mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '', mitos: stateArtikelKesehatan.edit.form.mythVsFact?.mitos || '',
fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || '', fakta: stateArtikelKesehatan.edit.form.mythVsFact?.fakta || ''
}, },
doctorSign: { doctorSign: {
content: stateArtikelKesehatan.edit.form.doctorSign?.content || '', content: stateArtikelKesehatan.edit.form.doctorSign?.content || ''
}, }
}); });
useEffect(() => { useEffect(() => {
@@ -84,29 +75,27 @@ function EditArtikelKesehatan() {
setFormData({ setFormData({
title: form.title, title: form.title,
content: form.content, content: form.content,
introduction: { introduction: { content: form.introduction?.content || '' },
content: form.introduction?.content || '',
},
symptom: { symptom: {
title: form.symptom?.title || '', title: form.symptom?.title || '',
content: form.symptom?.content || '', content: form.symptom?.content || ''
}, },
prevention: { prevention: {
title: form.prevention?.title || '', title: form.prevention?.title || '',
content: form.prevention?.content || '', content: form.prevention?.content || ''
}, },
firstAid: { firstAid: {
title: form.firstAid?.title || '', title: form.firstAid?.title || '',
content: form.firstAid?.content || '', content: form.firstAid?.content || ''
}, },
mythVsFact: { mythVsFact: {
title: form.mythVsFact?.title || '', title: form.mythVsFact?.title || '',
mitos: form.mythVsFact?.mitos || '', mitos: form.mythVsFact?.mitos || '',
fakta: form.mythVsFact?.fakta || '', fakta: form.mythVsFact?.fakta || ''
}, },
doctorSign: { doctorSign: {
content: form.doctorSign?.content || '', content: form.doctorSign?.content || ''
}, }
}); });
} }
} catch (error) { } catch (error) {
@@ -119,34 +108,7 @@ function EditArtikelKesehatan() {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateArtikelKesehatan.edit.form = { stateArtikelKesehatan.edit.form = { ...formData };
...stateArtikelKesehatan.edit.form,
title: formData.title,
content: formData.content,
introduction: {
content: formData.introduction.content,
},
symptom: {
title: formData.symptom.title,
content: formData.symptom.content,
},
prevention: {
title: formData.prevention.title,
content: formData.prevention.content,
},
firstAid: {
title: formData.firstAid.title,
content: formData.firstAid.content,
},
mythVsFact: {
title: formData.mythVsFact.title,
mitos: formData.mythVsFact.mitos,
fakta: formData.mythVsFact.fakta,
},
doctorSign: {
content: formData.doctorSign.content,
},
};
const success = await stateArtikelKesehatan.edit.submit(); const success = await stateArtikelKesehatan.edit.submit();
if (success) { if (success) {
toast.success("Artikel kesehatan berhasil diperbarui!"); toast.success("Artikel kesehatan berhasil diperbarui!");
@@ -157,214 +119,196 @@ function EditArtikelKesehatan() {
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data artikel kesehatan");
} }
}; };
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} />
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Button>
<Stack gap="xs"> </Tooltip>
<Title order={3}>Edit Artikel Kesehatan</Title> <Title order={4} ml="sm" c="dark">
Edit Artikel Kesehatan
</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">Judul</Text>} label="Judul"
placeholder="masukkan judul" placeholder="Masukkan judul artikel"
value={formData.title} value={formData.title}
onChange={(e) => { onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
setFormData(prev => ({ required
...prev,
title: e.target.value
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label="Deskripsi"
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi artikel"
value={formData.content} value={formData.content}
onChange={(e) => { onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
setFormData(prev => ({ required
...prev,
content: e.target.value
}));
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>} label="Pendahuluan"
placeholder="masukkan pendahuluan" placeholder="Masukkan pendahuluan"
value={formData.introduction.content} value={formData.introduction.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
introduction: { introduction: { ...prev.introduction, content: e.target.value }
...prev.introduction, }))
content: e.target.value }
}
}));
}}
/> />
{/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fw="bold">Gejala</Text>
<Stack gap="xs"> <Stack gap="xs">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul Gejala"
placeholder="masukkan judul gejala penyakit" placeholder="Masukkan judul gejala"
value={formData.symptom.title} value={formData.symptom.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
symptom: { symptom: { ...prev.symptom, title: e.target.value }
...prev.symptom, }))
title: e.target.value }
} />
})); <EditEditor
}} value={formData.symptom.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
symptom: { ...prev.symptom, content: e }
}))
}
/> />
<Box>
<Text fz="sm" fw="bold">Deskripsi Gejala</Text>
<EditEditor
value={formData.symptom.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
symptom: {
...prev.symptom,
content: e
}
}));
}}
/>
</Box>
</Stack> </Stack>
</Box> </Box>
{/* Pencegahan */}
<Box> <Box>
<Text fz="md" fw="bold">Pencegahan</Text> <Text fw="bold">Pencegahan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.prevention.title} value={formData.prevention.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
prevention: { prevention: { ...prev.prevention, title: e.target.value }
...prev.prevention, }))
title: e.target.value }
}
}));
}}
/> />
<EditEditor <EditEditor
value={formData.prevention.content} value={formData.prevention.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
prevention: { prevention: { ...prev.prevention, content: e }
...prev.prevention, }))
content: e }
}
}));
}}
/> />
</Box> </Box>
{/* Pertolongan Pertama */}
<Box> <Box>
<Text fz="md" fw="bold">Pertolongan Pertama</Text> <Text fw="bold">Pertolongan Pertama</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.firstAid.title} value={formData.firstAid.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
firstAid: { firstAid: { ...prev.firstAid, title: e.target.value }
...prev.firstAid, }))
title: e.target.value }
}
}));
}}
/> />
<EditEditor <EditEditor
value={formData.firstAid.content} value={formData.firstAid.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
firstAid: { firstAid: { ...prev.firstAid, content: e }
...prev.firstAid, }))
content: e }
}
}));
}}
/> />
</Box> </Box>
{/* Mitos vs Fakta */}
<Box> <Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text> <Text fw="bold">Mitos vs Fakta</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul"
value={formData.mythVsFact.title} value={formData.mythVsFact.title}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
mythVsFact: { mythVsFact: { ...prev.mythVsFact, title: e.target.value }
...prev.mythVsFact, }))
title: e.target.value }
} />
})); <Text fw="500">Mitos</Text>
}} <EditEditor
value={formData.mythVsFact.mitos}
onChange={(e) =>
setFormData(prev => ({
...prev,
mythVsFact: { ...prev.mythVsFact, mitos: e }
}))
}
/>
<Text fw="500">Fakta</Text>
<EditEditor
value={formData.mythVsFact.fakta}
onChange={(e) =>
setFormData(prev => ({
...prev,
mythVsFact: { ...prev.mythVsFact, fakta: e }
}))
}
/> />
<Box>
<Text>
Mitos
</Text>
<EditEditor
value={formData.mythVsFact.mitos}
onChange={(e) => {
setFormData(prev => ({
...prev,
mythVsFact: {
...prev.mythVsFact,
mitos: e
}
}));
}}
/>
</Box>
<Box>
<Text>
Fakta
</Text>
<EditEditor
value={formData.mythVsFact.fakta}
onChange={(e) => {
setFormData(prev => ({
...prev,
mythVsFact: {
...prev.mythVsFact,
fakta: e
}
}));
}}
/>
</Box>
</Box> </Box>
{/* Kapan harus ke dokter */}
<Box> <Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text> <Text fw="bold">Kapan Harus Ke Dokter</Text>
<EditEditor <EditEditor
value={formData.doctorSign.content} value={formData.doctorSign.content}
onChange={(e) => { onChange={(e) =>
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
doctorSign: { doctorSign: { ...prev.doctorSign, content: e }
...prev.doctorSign, }))
content: e }
}
}));
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan {/* Save button */}
</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

@@ -2,134 +2,181 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
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';
function DetailArtikelKesehatan() { function DetailArtikelKesehatan() {
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const state = useProxy(artikelKesehatanState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => { useShallowEffect(() => {
stateArtikelKesehatan.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateArtikelKesehatan.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan") router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
} }
} };
if (!stateArtikelKesehatan.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 Back */}
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Artikel Kesehatan</Text> Kembali
{stateArtikelKesehatan.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fz={"lg"} fw={"bold"}>Judul</Text> withBorder
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.title}</Text> w={{ base: '100%', md: '50%' }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> radius="md"
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.content }} /> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fz={"lg"} fw={"bold"}>Pendahuluan</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.introduction.content }} /> Detail Artikel Kesehatan
</Box> </Text>
<Box>
<Stack gap={"xs"}> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Text fz={"lg"} fw={"bold"}>Gejala</Text> <Stack gap="sm">
<Text fz={"md"} fw={"bold"}>Judul Gejala</Text> {/* Judul */}
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.symptom.title}</Text> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi Gejala</Text> <Text fz="lg" fw="bold">Judul</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.symptom.content }} /> <Text fz="md" c="dimmed">{data.title}</Text>
</Stack> </Box>
</Box>
<Box> {/* Deskripsi */}
<Stack gap={"xs"}> <Box>
<Text fz={"lg"} fw={"bold"}>Pencegahan</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"md"} fw={"bold"}>Judul Pencegahan</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.content }} />
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.prevention.title}</Text> </Box>
<Text fz={"md"} fw={"bold"}>Deskripsi Pencegahan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.prevention.content }} /> {/* Pendahuluan */}
</Stack> <Box>
</Box> <Text fz="lg" fw="bold">Pendahuluan</Text>
<Box> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.introduction?.content }} />
<Stack gap={"xs"}> </Box>
<Text fz={"lg"} fw={"bold"}>Pertolongan Pertama</Text>
<Text fz={"md"} fw={"bold"}>Judul Pertolongan Pertama</Text> {/* Gejala */}
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.firstaid.title}</Text> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi Pertolongan Pertama</Text> <Text fz="lg" fw="bold">Gejala</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.firstaid.content }} /> <Text fz="md" fw="bold">Judul</Text>
</Stack> <Text fz="md" c="dimmed">{data.symptom?.title}</Text>
</Box> <Text fz="md" fw="bold">Deskripsi</Text>
<Box> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.symptom?.content }} />
<Stack gap={"xs"}> </Box>
<Text fz={"lg"} fw={"bold"}>Mitos dan Fakta</Text>
<Text fz={"md"} fw={"bold"}>Judul Mitos dan Fakta</Text> {/* Pencegahan */}
<Text fz={"md"}>{stateArtikelKesehatan.findUnique.data.mythvsfact.title}</Text> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi Mitos</Text> <Text fz="lg" fw="bold">Pencegahan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.mitos }} /> <Text fz="md" fw="bold">Judul</Text>
<Text fz={"md"} fw={"bold"}>Deskripsi Fakta</Text> <Text fz="md" c="dimmed">{data.prevention?.title}</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.mythvsfact.fakta }} /> <Text fz="md" fw="bold">Deskripsi</Text>
</Stack> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.prevention?.content }} />
</Box> </Box>
<Box>
<Stack gap={"xs"}> {/* Pertolongan Pertama */}
<Text fz={"lg"} fw={"bold"}>Kapan Harus ke Dokter</Text> <Box>
<Text fz={"md"} fw={"bold"}>Deskripsi Kapan Harus ke Dokter</Text> <Text fz="lg" fw="bold">Pertolongan Pertama</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateArtikelKesehatan.findUnique.data.doctorsign.content }} /> <Text fz="md" fw="bold">Judul</Text>
</Stack> <Text fz="md" c="dimmed">{data.firstaid?.title}</Text>
</Box> <Text fz="md" fw="bold">Deskripsi</Text>
<Box> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.firstaid?.content }} />
<Flex gap={"xs"}> </Box>
<Button color="red" onClick={() => {
if (stateArtikelKesehatan.findUnique.data) { {/* Mitos vs Fakta */}
setSelectedId(stateArtikelKesehatan.findUnique.data.id) <Box>
setModalHapus(true) <Text fz="lg" fw="bold">Mitos dan Fakta</Text>
} <Text fz="md" fw="bold">Judul</Text>
}}> <Text fz="md" c="dimmed">{data.mythvsfact?.title}</Text>
<IconX size={20} /> <Text fz="md" fw="bold">Mitos</Text>
</Button> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.mythvsfact?.mitos }} />
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${stateArtikelKesehatan.findUnique.data?.id}/edit`)} color="green"> <Text fz="md" fw="bold">Fakta</Text>
<IconEdit size={20} /> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.mythvsfact?.fakta }} />
</Button> </Box>
</Flex>
</Box> {/* Kapan ke Dokter */}
</Stack> <Box>
</Paper> <Text fz="lg" fw="bold">Kapan Harus ke Dokter</Text>
) : null} <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.doctorsign?.content }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Artikel" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Artikel" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -2,101 +2,129 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
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 { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateArtikelKesehatan() { function CreateArtikelKesehatan() {
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const stateArtikelKesehatan = useProxy(artikelKesehatanState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateArtikelKesehatan.create.form = { stateArtikelKesehatan.create.form = {
title: "", title: '',
content: "", content: '',
introduction: { introduction: {
content: "", content: '',
}, },
symptom: { symptom: {
title: "", title: '',
content: "", content: '',
}, },
prevention: { prevention: {
title: "", title: '',
content: "", content: '',
}, },
firstAid: { firstAid: {
title: "", title: '',
content: "", content: '',
}, },
mythVsFact: { mythVsFact: {
title: "", title: '',
mitos: "", mitos: '',
fakta: "", fakta: '',
}, },
doctorSign: { doctorSign: {
content: "" content: '',
} },
}; };
}; };
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await stateArtikelKesehatan.create.submit(); await stateArtikelKesehatan.create.submit();
toast.success('Data berhasil disimpan');
toast.success("Data berhasil disimpan");
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan'); router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<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
</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">
Tambah Artikel Kesehatan
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Artikel Kesehatan</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
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul"}
placeholder="masukkan judul" placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.title} value={stateArtikelKesehatan.create.form.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.title = e.target.value; stateArtikelKesehatan.create.form.title = e.target.value;
}} }}
required
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Deskripsi</Text>} label={"Deskripsi"}
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi"
value={stateArtikelKesehatan.create.form.content} value={stateArtikelKesehatan.create.form.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.content = e.target.value; stateArtikelKesehatan.create.form.content = e.target.value;
}} }}
required
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Pendahuluan</Text>} label={"Pendahuluan"}
placeholder="masukkan pendahuluan" placeholder="Masukkan pendahuluan"
required
value={stateArtikelKesehatan.create.form.introduction.content} value={stateArtikelKesehatan.create.form.introduction.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.introduction.content = e.target.value; stateArtikelKesehatan.create.form.introduction.content = e.target.value;
}} }}
/> />
{/* Gejala */}
<Box> <Box>
<Text fz="md" fw="bold">Gejala</Text> <Text fz="md" fw="bold">Gejala</Text>
<Stack gap="xs"> <Stack gap="sm">
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Gejala"}
placeholder="masukkan judul gejala penyakit" required
placeholder="Masukkan judul gejala penyakit"
value={stateArtikelKesehatan.create.form.symptom.title} value={stateArtikelKesehatan.create.form.symptom.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.symptom.title = e.target.value; stateArtikelKesehatan.create.form.symptom.title = e.target.value;
@@ -114,54 +142,62 @@ function CreateArtikelKesehatan() {
</Stack> </Stack>
</Box> </Box>
<Box> {/* Pencegahan */}
<Stack gap="xs">
<Text fz="md" fw="bold">Pencegahan</Text> <Text fz="md" fw="bold">Pencegahan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Pencegahan"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.prevention.title} value={stateArtikelKesehatan.create.form.prevention.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.title = e.target.value; stateArtikelKesehatan.create.form.prevention.title = e.target.value;
}} }}
/> />
<Text fz="sm">Deskripsi Pencegahan</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.prevention.content} value={stateArtikelKesehatan.create.form.prevention.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.prevention.content = e; stateArtikelKesehatan.create.form.prevention.content = e;
}} }}
/> />
</Box> </Stack>
<Box>
{/* Pertolongan Pertama */}
<Stack gap={"xs"}>
<Text fz="md" fw="bold">Pertolongan Pertama</Text> <Text fz="md" fw="bold">Pertolongan Pertama</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Pertolongan Pertama"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.firstAid.title} value={stateArtikelKesehatan.create.form.firstAid.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.title = e.target.value; stateArtikelKesehatan.create.form.firstAid.title = e.target.value;
}} }}
/> />
<Text fz="sm">Deskripsi Pertolongan Pertama</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.firstAid.content} value={stateArtikelKesehatan.create.form.firstAid.content}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.firstAid.content = e; stateArtikelKesehatan.create.form.firstAid.content = e;
}} }}
/> />
</Box> </Stack>
{/* Mitos vs Fakta */}
<Box> <Box>
<Text fz="md" fw="bold">Mitos dan Fakta</Text> <Text fz="md" fw="bold">Mitos dan Fakta</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Judul</Text>} label={"Judul Mitos dan Fakta"}
placeholder="masukkan judul" required
placeholder="Masukkan judul"
value={stateArtikelKesehatan.create.form.mythVsFact.title} value={stateArtikelKesehatan.create.form.mythVsFact.title}
onChange={(e) => { onChange={(e) => {
stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value; stateArtikelKesehatan.create.form.mythVsFact.title = e.target.value;
}} }}
/> />
<Box> <Box mt="sm">
<Text> <Text fz="sm" fw="bold">Mitos</Text>
Mitos
</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.mitos} value={stateArtikelKesehatan.create.form.mythVsFact.mitos}
onChange={(e) => { onChange={(e) => {
@@ -169,10 +205,8 @@ function CreateArtikelKesehatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box mt="sm">
<Text> <Text fz="sm" fw="bold">Fakta</Text>
Fakta
</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.mythVsFact.fakta} value={stateArtikelKesehatan.create.form.mythVsFact.fakta}
onChange={(e) => { onChange={(e) => {
@@ -181,8 +215,10 @@ function CreateArtikelKesehatan() {
/> />
</Box> </Box>
</Box> </Box>
{/* Kapan Harus ke Dokter */}
<Box> <Box>
<Text fz="md" fw="bold">Kapan Harus Ke Dokter</Text> <Text fz="md" fw="bold">Kapan Harus ke Dokter</Text>
<CreateEditor <CreateEditor
value={stateArtikelKesehatan.create.form.doctorSign.content} value={stateArtikelKesehatan.create.form.doctorSign.content}
onChange={(e) => { onChange={(e) => {
@@ -191,9 +227,21 @@ function CreateArtikelKesehatan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Submit Button */}
Simpan <Group justify="right">
</Button> <Button
type="submit"
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,99 +1,164 @@
'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 } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan'; import artikelKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/artikelKesehatan';
import { useState } from 'react'; import { useState } from 'react';
function ArtikelKesehatan() { function ArtikelKesehatan() {
const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Artikel Kesehatan' title='Artikel Kesehatan'
placeholder='pencarian' placeholder='Cari judul atau konten...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListArtikelKesehatan search={search} /> <ListArtikelKesehatan search={search} />
</Box> </Box>
); );
} }
function ListArtikelKesehatan({ search }: { search: string }) { function ListArtikelKesehatan({ search }: { search: string }) {
const stateArtikelKesehatan = useProxy(artikelKesehatanState) const stateArtikel = useProxy(artikelKesehatanState);
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = stateArtikel.findMany;
useShallowEffect(() => { useShallowEffect(() => {
stateArtikelKesehatan.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (stateArtikelKesehatan.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.title.toLowerCase().includes(keyword) || <Stack py={10}>
item.content.toLowerCase().includes(keyword) <Skeleton height={600} radius="md" />
</Stack>
); );
});
if (!stateArtikelKesehatan.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500} />
</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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Artikel Kesehatan' <Title order={4}>Daftar Artikel Kesehatan</Title>
href='/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create' <Tooltip label="Tambah Artikel Kesehatan" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/create')}
<TableTh>Judul</TableTh> >
<TableTh>Content</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Group>
<TableTbody>
{filteredData.map((item) => ( {/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Konten</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate={'end'} lineClamp={1} fz={'h5'}>{item.title}</Text> {item.title}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={200}> <Text truncate fz="sm" c="dimmed" lineClamp={1}>
<Text truncate={'end'} lineClamp={1} fz={'h5'}>{item.content}</Text> {item.content}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/artikel_kesehatan/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={3}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada artikel yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<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>
) );
} }
export default ArtikelKesehatan; export default ArtikelKesehatan;

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
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';
@@ -18,20 +28,14 @@ interface FasilitasKesehatanFormBase {
alamat: string; alamat: string;
jamOperasional: string; jamOperasional: string;
}; };
layananUnggulan: { layananUnggulan: { content: string };
content: string;
};
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: string; name: string;
specialist: string; specialist: string;
jadwal: string; jadwal: string;
}; };
fasilitasPendukung: { fasilitasPendukung: { content: string };
content: string; prosedurPendaftaran: { content: string };
};
prosedurPendaftaran: {
content: string;
};
tarifDanLayanan: { tarifDanLayanan: {
layanan: string; layanan: string;
tarif: string; tarif: string;
@@ -86,20 +90,14 @@ function EditFasilitasKesehatan() {
alamat: form.informasiUmum?.alamat || '', alamat: form.informasiUmum?.alamat || '',
jamOperasional: form.informasiUmum?.jamOperasional || '', jamOperasional: form.informasiUmum?.jamOperasional || '',
}, },
layananUnggulan: { layananUnggulan: { content: form.layananUnggulan?.content || '' },
content: form.layananUnggulan?.content || '',
},
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: form.dokterdanTenagaMedis?.name || '', name: form.dokterdanTenagaMedis?.name || '',
specialist: form.dokterdanTenagaMedis?.specialist || '', specialist: form.dokterdanTenagaMedis?.specialist || '',
jadwal: form.dokterdanTenagaMedis?.jadwal || '', jadwal: form.dokterdanTenagaMedis?.jadwal || '',
}, },
fasilitasPendukung: { fasilitasPendukung: { content: form.fasilitasPendukung?.content || '' },
content: form.fasilitasPendukung?.content || '', prosedurPendaftaran: { content: form.prosedurPendaftaran?.content || '' },
},
prosedurPendaftaran: {
content: form.prosedurPendaftaran?.content || '',
},
tarifDanLayanan: { tarifDanLayanan: {
layanan: form.tarifDanLayanan?.layanan || '', layanan: form.tarifDanLayanan?.layanan || '',
tarif: form.tarifDanLayanan?.tarif || '', tarif: form.tarifDanLayanan?.tarif || '',
@@ -118,30 +116,7 @@ function EditFasilitasKesehatan() {
try { try {
stateFasilitasKesehatan.edit.form = { stateFasilitasKesehatan.edit.form = {
...stateFasilitasKesehatan.edit.form, ...stateFasilitasKesehatan.edit.form,
name: formData.name, ...formData,
informasiUmum: {
fasilitas: formData.informasiUmum.fasilitas,
alamat: formData.informasiUmum.alamat,
jamOperasional: formData.informasiUmum.jamOperasional,
},
layananUnggulan: {
content: formData.layananUnggulan.content,
},
dokterdanTenagaMedis: {
name: formData.dokterdanTenagaMedis.name,
specialist: formData.dokterdanTenagaMedis.specialist,
jadwal: formData.dokterdanTenagaMedis.jadwal,
},
fasilitasPendukung: {
content: formData.fasilitasPendukung.content,
},
prosedurPendaftaran: {
content: formData.prosedurPendaftaran.content,
},
tarifDanLayanan: {
layanan: formData.tarifDanLayanan.layanan,
tarif: formData.tarifDanLayanan.tarif,
},
}; };
const success = await stateFasilitasKesehatan.edit.submit(); const success = await stateFasilitasKesehatan.edit.submit();
if (success) { if (success) {
@@ -150,208 +125,197 @@ function EditFasilitasKesehatan() {
} }
} catch (error) { } catch (error) {
console.error("Error updating fasilitas kesehatan:", error); console.error("Error updating fasilitas kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data fasilitas kesehatan"); toast.error("Terjadi kesalahan saat memperbarui data fasilitas kesehatan");
} }
}; };
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} />
<Stack gap="xs"> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Fasilitas Kesehatan</Title> Edit Fasilitas Kesehatan
</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
label="Nama Fasilitas Kesehatan"
placeholder="Masukkan nama fasilitas kesehatan"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
required
/>
{/* Informasi Umum */}
<Box>
<Text fw="bold" mb={5}>Informasi Umum</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Fasilitas Kesehatan</Text>} label="Fasilitas"
placeholder="masukkan nama fasilitas kesehatan" value={formData.informasiUmum.fasilitas}
value={formData.name} onChange={(e) =>
onChange={(e) => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
name: e.target.value informasiUmum: { ...prev.informasiUmum, fasilitas: e.target.value },
})); }))
}} }
/> />
<Box> <TextInput
<Text fz="md" fw="bold">Informasi Umum</Text> label="Alamat"
<TextInput value={formData.informasiUmum.alamat}
label={<Text fz="sm" fw="bold">Fasilitas</Text>} onChange={(e) =>
placeholder="masukkan fasilitas" setFormData(prev => ({
value={formData.informasiUmum.fasilitas} ...prev,
onChange={(e) => { informasiUmum: { ...prev.informasiUmum, alamat: e.target.value },
setFormData(prev => ({ }))
...prev, }
informasiUmum: { />
...prev.informasiUmum, <TextInput
fasilitas: e.target.value label="Jam Operasional"
} value={formData.informasiUmum.jamOperasional}
})); onChange={(e) =>
}} setFormData(prev => ({
/> ...prev,
<TextInput informasiUmum: { ...prev.informasiUmum, jamOperasional: e.target.value },
label={<Text fz="sm" fw="bold">Alamat</Text>} }))
placeholder="masukkan alamat" }
value={formData.informasiUmum.alamat} />
onChange={(e) => { </Box>
setFormData(prev => ({
...prev,
informasiUmum: {
...prev.informasiUmum,
alamat: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jam Operasional</Text>}
placeholder="masukkan jam operasional"
value={formData.informasiUmum.jamOperasional}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiUmum: {
...prev.informasiUmum,
jamOperasional: e.target.value
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Layanan Unggulan</Text>
<EditEditor
value={formData.layananUnggulan.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
layananUnggulan: {
...prev.layananUnggulan,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Dokter dan Tenaga Medis</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
placeholder="masukkan nama dokter"
value={formData.dokterdanTenagaMedis.name}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
name: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>}
placeholder="masukkan specialist"
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
specialist: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jadwal</Text>}
placeholder="masukkan jadwal"
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) => {
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: {
...prev.dokterdanTenagaMedis,
jadwal: e.target.value
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Fasilitas Pendukung</Text>
<EditEditor
value={formData.fasilitasPendukung.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
fasilitasPendukung: {
...prev.fasilitasPendukung,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Prosedur Pendaftaran</Text>
<EditEditor
value={formData.prosedurPendaftaran.content}
onChange={(e) => {
setFormData(prev => ({
...prev,
prosedurPendaftaran: {
...prev.prosedurPendaftaran,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Tarif dan Layanan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Tarif</Text>}
placeholder="masukkan tarif"
value={formData.tarifDanLayanan.tarif}
onChange={(e) => {
setFormData(prev => ({
...prev,
tarifDanLayanan: {
...prev.tarifDanLayanan,
tarif: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Layanan</Text>}
placeholder="masukkan layanan"
value={formData.tarifDanLayanan.layanan}
onChange={(e) => {
setFormData(prev => ({
...prev,
tarifDanLayanan: {
...prev.tarifDanLayanan,
layanan: e.target.value
}
}));
}}
/>
</Box>
<Button {/* Layanan Unggulan */}
onClick={handleSubmit} <Box>
bg={colors['blue-button']} <Text fw="bold" mb={5}>Layanan Unggulan</Text>
loading={stateFasilitasKesehatan.edit.loading} <EditEditor
> value={formData.layananUnggulan.content}
Simpan onChange={(e) =>
</Button> setFormData(prev => ({
</Stack> ...prev,
</Paper> layananUnggulan: { content: e },
</Stack> }))
}
/>
</Box>
{/* Dokter dan Tenaga Medis */}
<Box>
<Text fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
<TextInput
label="Nama Dokter"
value={formData.dokterdanTenagaMedis.name}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, name: e.target.value },
}))
}
/>
<TextInput
label="Specialist"
value={formData.dokterdanTenagaMedis.specialist}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, specialist: e.target.value },
}))
}
/>
<TextInput
label="Jadwal"
value={formData.dokterdanTenagaMedis.jadwal}
onChange={(e) =>
setFormData(prev => ({
...prev,
dokterdanTenagaMedis: { ...prev.dokterdanTenagaMedis, jadwal: e.target.value },
}))
}
/>
</Box>
{/* Fasilitas Pendukung */}
<Box>
<Text fw="bold" mb={5}>Fasilitas Pendukung</Text>
<EditEditor
value={formData.fasilitasPendukung.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
fasilitasPendukung: { content: e },
}))
}
/>
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fw="bold" mb={5}>Prosedur Pendaftaran</Text>
<EditEditor
value={formData.prosedurPendaftaran.content}
onChange={(e) =>
setFormData(prev => ({
...prev,
prosedurPendaftaran: { content: e },
}))
}
/>
</Box>
{/* Tarif dan Layanan */}
<Box>
<Text fw="bold" mb={5}>Tarif dan Layanan</Text>
<TextInput
label="Tarif"
value={formData.tarifDanLayanan.tarif}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, tarif: e.target.value },
}))
}
/>
<TextInput
label="Layanan"
value={formData.tarifDanLayanan.layanan}
onChange={(e) =>
setFormData(prev => ({
...prev,
tarifDanLayanan: { ...prev.tarifDanLayanan, layanan: e.target.value },
}))
}
/>
</Box>
{/* Tombol Simpan */}
<Group justify="flex-end">
<Button
onClick={handleSubmit}
radius="md"
size="md"
loading={stateFasilitasKesehatan.edit.loading}
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
); );
} }

View File

@@ -2,133 +2,169 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Grid, GridCol, 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';
function DetailFasilitasKesehatan() { function DetailFasilitasKesehatan() {
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const state = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => { useShallowEffect(() => {
stateFasilitasKesehatan.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateFasilitasKesehatan.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan") router.push(
'/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'
);
} }
} };
if (!stateFasilitasKesehatan.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 Back */}
<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']} p={'md'}> mb={15}
<Stack> >
<Grid> Kembali
<GridCol span={12}> </Button>
<Text fz={"xl"} fw={"bold"}>Detail Fasilitas Kesehatan</Text>
</GridCol>
{/* <GridCol span={12}>
<Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/dokter-tenaga-medis`)}>
Tambah Dokter
</Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${params?.id}/layanan-unggulan/create`)}>
Tambah Layanan
</Button>
</Flex>
</GridCol> */}
</Grid>
{stateFasilitasKesehatan.findUnique.data ? (
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Fasilitas Kesehatan</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Informasi Umum</Text>
<Text fz={"md"} fw={"bold"}>Fasilitas</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.fasilitas}</Text>
<Text fz={"md"} fw={"bold"}>Alamat</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.alamat}</Text>
<Text fz={"md"} fw={"bold"}>Jam Operasional</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.informasiumum.jamOperasional}</Text>
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Layanan Unggulan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.layananunggulan.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Fasilitas Pendukung</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.fasilitaspendukung.content }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Prosedur Pendaftaran</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateFasilitasKesehatan.findUnique.data.prosedurpendaftaran.content }} />
</Box>
<Box>
<Text fz={"md"} fw={"bold"}>Dokter dan Tenaga Medis</Text>
<Text fz={"md"} fw={"bold"}>Nama Dokter</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.name}</Text>
<Text fz={"md"} fw={"bold"}>Specialist</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.specialist}</Text>
<Text fz={"md"} fw={"bold"}>Jadwal</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.dokterdantenagamedis.jadwal}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tarif dan Layanan</Text>
<Text fz={"md"} fw={"bold"}>Layanan</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.tarifdanlayanan.layanan}</Text>
<Text fz={"md"} fw={"bold"}>Tarif</Text>
<Text fz={"md"}>{stateFasilitasKesehatan.findUnique.data.tarifdanlayanan.tarif}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red" onClick={() => {
if (stateFasilitasKesehatan.findUnique.data) {
setSelectedId(stateFasilitasKesehatan.findUnique.data.id)
setModalHapus(true)
}
}}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${stateFasilitasKesehatan.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
{/* Wrapper Detail */}
<Paper
withBorder
w={{ base: '100%', md: '70%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Fasilitas Kesehatan
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Fasilitas</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Informasi Umum</Text>
<Text fz="md" fw="bold">Fasilitas</Text>
<Text fz="md" c="dimmed">{data.informasiumum?.fasilitas || '-'}</Text>
<Text fz="md" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data.informasiumum?.alamat || '-'}</Text>
<Text fz="md" fw="bold">Jam Operasional</Text>
<Text fz="md" c="dimmed">{data.informasiumum?.jamOperasional || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Layanan Unggulan</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.layananunggulan?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Fasilitas Pendukung</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.fasilitaspendukung?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.prosedurpendaftaran?.content || '-' }} />
</Box>
<Box>
<Text fz="lg" fw="bold">Dokter & Tenaga Medis</Text>
<Text fz="md" fw="bold">Nama</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.name || '-'}</Text>
<Text fz="md" fw="bold">Spesialis</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.specialist || '-'}</Text>
<Text fz="md" fw="bold">Jadwal</Text>
<Text fz="md" c="dimmed">{data.dokterdantenagamedis?.jadwal || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tarif & Layanan</Text>
<Text fz="md" fw="bold">Layanan</Text>
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.layanan || '-'}</Text>
<Text fz="md" fw="bold">Tarif</Text>
<Text fz="md" c="dimmed">{data.tarifdanlayanan?.tarif || '-'}</Text>
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -2,7 +2,17 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
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 { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -10,174 +20,196 @@ import { useProxy } from 'valtio/utils';
function CreateFasilitasKesehatan() { function CreateFasilitasKesehatan() {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateFasilitasKesehatan.create.form = { stateFasilitasKesehatan.create.form = {
name: "", name: '',
informasiUmum: { informasiUmum: {
fasilitas: "", fasilitas: '',
alamat: "", alamat: '',
jamOperasional: "", jamOperasional: '',
}, },
layananUnggulan: { layananUnggulan: {
content: "", content: '',
}, },
dokterdanTenagaMedis: { dokterdanTenagaMedis: {
name: "", name: '',
specialist: "", specialist: '',
jadwal: "", jadwal: '',
}, },
fasilitasPendukung: { fasilitasPendukung: {
content: "", content: '',
}, },
prosedurPendaftaran: { prosedurPendaftaran: {
content: "", content: '',
}, },
tarifDanLayanan: { tarifDanLayanan: {
layanan: "", layanan: '',
tarif: "", tarif: '',
}, },
}; };
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
await stateFasilitasKesehatan.create.submit(); await stateFasilitasKesehatan.create.submit();
toast.success('Data berhasil disimpan');
toast.success("Data berhasil disimpan");
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan'); router.push('/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<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
</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">
Tambah Data Fasilitas Kesehatan
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Fasilitas Kesehatan</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
label={<Text fz="sm" fw="bold">Nama Fasilitas Kesehatan</Text>} label={"Nama Fasilitas Kesehatan"}
placeholder="masukkan nama fasilitas kesehatan" placeholder="Masukkan nama fasilitas kesehatan"
value={stateFasilitasKesehatan.create.form.name} value={stateFasilitasKesehatan.create.form.name}
onChange={(e) => { onChange={(e) => (stateFasilitasKesehatan.create.form.name = e.target.value)}
stateFasilitasKesehatan.create.form.name = e.target.value; required
}}
/> />
{/* Informasi Umum */}
<Box> <Box>
<Text fz="md" fw="bold">Informasi Umum</Text> <Text fz="md" fw="bold" mb={5}>Informasi Umum</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Fasilitas</Text>} label="Fasilitas"
placeholder="masukkan fasilitas" placeholder="Masukkan fasilitas"
value={stateFasilitasKesehatan.create.form.informasiUmum.fasilitas} value={stateFasilitasKesehatan.create.form.informasiUmum.fasilitas}
onChange={(e) => { onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value)}
stateFasilitasKesehatan.create.form.informasiUmum.fasilitas = e.target.value; required
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" placeholder="Masukkan alamat"
value={stateFasilitasKesehatan.create.form.informasiUmum.alamat} value={stateFasilitasKesehatan.create.form.informasiUmum.alamat}
onChange={(e) => { onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value)}
stateFasilitasKesehatan.create.form.informasiUmum.alamat = e.target.value; required
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Operasional</Text>} label="Jam Operasional"
placeholder="masukkan jam operasional" placeholder="Masukkan jam operasional"
value={stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional} value={stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional}
onChange={(e) => { onChange={(e) => (stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value)}
stateFasilitasKesehatan.create.form.informasiUmum.jamOperasional = e.target.value; required
}}
/> />
</Box> </Box>
{/* Layanan Unggulan */}
<Box> <Box>
<Text fz="md" fw="bold">Layanan Unggulan</Text> <Text fz="md" fw="bold" mb={5}>Layanan Unggulan</Text>
<CreateEditor <CreateEditor
value={stateFasilitasKesehatan.create.form.layananUnggulan.content} value={stateFasilitasKesehatan.create.form.layananUnggulan.content}
onChange={(e) => { onChange={(val) => (stateFasilitasKesehatan.create.form.layananUnggulan.content = val)}
stateFasilitasKesehatan.create.form.layananUnggulan.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Dokter dan Tenaga Medis</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama Dokter</Text>}
placeholder="masukkan nama dokter"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Specialist</Text>}
placeholder="masukkan specialist"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Jadwal</Text>}
placeholder="masukkan jadwal"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
onChange={(e) => {
stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Fasilitas Pendukung</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.fasilitasPendukung.content}
onChange={(e) => {
stateFasilitasKesehatan.create.form.fasilitasPendukung.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Prosedur Pendaftaran</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.prosedurPendaftaran.content}
onChange={(e) => {
stateFasilitasKesehatan.create.form.prosedurPendaftaran.content = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Tarif dan Layanan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Tarif</Text>}
placeholder="masukkan tarif"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
onChange={(e) => {
stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value;
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Layanan</Text>}
placeholder="masukkan layanan"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
onChange={(e) => {
stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value;
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Dokter dan Tenaga Medis */}
Simpan <Box>
</Button> <Text fz="md" fw="bold" mb={5}>Dokter dan Tenaga Medis</Text>
<TextInput
label="Nama Dokter"
placeholder="Masukkan nama dokter"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.name = e.target.value)}
required
/>
<TextInput
label="Spesialis"
placeholder="Masukkan spesialis"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.specialist = e.target.value)}
required
/>
<TextInput
label="Jadwal"
placeholder="Masukkan jadwal"
value={stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal}
onChange={(e) => (stateFasilitasKesehatan.create.form.dokterdanTenagaMedis.jadwal = e.target.value)}
required
/>
</Box>
{/* Fasilitas Pendukung */}
<Box>
<Text fz="md" fw="bold" mb={5}>Fasilitas Pendukung</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.fasilitasPendukung.content}
onChange={(val) => (stateFasilitasKesehatan.create.form.fasilitasPendukung.content = val)}
/>
</Box>
{/* Prosedur Pendaftaran */}
<Box>
<Text fz="md" fw="bold" mb={5}>Prosedur Pendaftaran</Text>
<CreateEditor
value={stateFasilitasKesehatan.create.form.prosedurPendaftaran.content}
onChange={(val) => (stateFasilitasKesehatan.create.form.prosedurPendaftaran.content = val)}
/>
</Box>
{/* Tarif dan Layanan */}
<Box>
<Text fz="md" fw="bold" mb={5}>Tarif dan Layanan</Text>
<TextInput
label="Tarif"
placeholder="Masukkan tarif"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.tarif = e.target.value)}
required
/>
<TextInput
label="Layanan"
placeholder="Masukkan layanan"
value={stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan}
onChange={(e) => (stateFasilitasKesehatan.create.form.tarifDanLayanan.layanan = e.target.value)}
required
/>
</Box>
{/* Submit */}
<Group justify="right" mt="md">
<Button
type="submit"
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,114 +1,172 @@
'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,
Paper,
Pagination,
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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, 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 fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan'; import fasilitasKesehatanState from '../../../_state/kesehatan/data_kesehatan_warga/fasilitasKesehatan';
function FasilitasKesehatan() { function FasilitasKesehatan() {
const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
// const router = useRouter();
return ( return (
<Box> <Box>
{/* <Grid> {/* Tombol Back */}
<GridCol span={12}> <Box mb={10}>
<HeaderSearch <Button variant="subtle" onClick={() => router.back()}>
title='Fasilitas Kesehatan' <IconArrowBack color={colors["blue-button"]} size={25} />
placeholder='pencarian' </Button>
searchIcon={<IconSearch size={20} />} </Box>
value={search}
onChange={(e) => setSearch(e.currentTarget.value)} {/* Header Search */}
/>
</GridCol>
<GridCol span={12}>
<Flex gap={"xs"}>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/dokter-tenaga-medis`)}>
<IconList size={20} /> List Dokter
</Button>
<Button color={colors['blue-button']} onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/tarif-layanan`)}>
<IconList size={20} /> List Layanan
</Button>
</Flex>
</GridCol>
</Grid> */}
<HeaderSearch <HeaderSearch
title='Fasilitas Kesehatan' title='Fasilitas Kesehatan'
placeholder='pencarian' placeholder='Cari nama, alamat, atau jam operasional...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListFasilitasKesehatan search={search} /> <ListFasilitasKesehatan search={search} />
</Box> </Box>
); );
} }
function ListFasilitasKesehatan({ search }: { search: string }) { function ListFasilitasKesehatan({ search }: { search: string }) {
const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan) const stateFasilitasKesehatan = useProxy(fasilitasKesehatanState.fasilitasKesehatan)
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = stateFasilitasKesehatan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
stateFasilitasKesehatan.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (stateFasilitasKesehatan.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.informasiumum.alamat.toLowerCase().includes(keyword) ||
item.informasiumum.jamOperasional.toLowerCase().includes(keyword)
);
});
if (!stateFasilitasKesehatan.findMany.data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Fasilitas Kesehatan' <Title order={4}>Daftar Fasilitas Kesehatan</Title>
href='/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create' <Tooltip label="Tambah Fasilitas Kesehatan" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() =>
<TableTh>Fasilitas Kesehatan</TableTh> router.push(
<TableTh>Dokter</TableTh> '/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/create'
<TableTh>Layanan</TableTh> )
<TableTh>Detail</TableTh> }
</TableTr> >
</TableThead> Tambah Baru
<TableTbody> </Button>
{filteredData.map((item) => ( </Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Fasilitas Kesehatan</TableTh>
<TableTh>Dokter</TableTh>
<TableTh>Layanan</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.dokterdantenagamedis.name}</TableTd>
<TableTd>{item.tarifdanlayanan.layanan}</TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`)}> <Text fw={500} truncate="end" lineClamp={1}>
<IconDeviceImacCog size={25} /> {item.name}
</Text>
</TableTd>
<TableTd>{item.dokterdantenagamedis?.name || '-'}</TableTd>
<TableTd>{item.tarifdanlayanan?.layanan || '-'}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/fasilitas_kesehatan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">
Tidak ada fasilitas kesehatan yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -2,7 +2,16 @@
'use client' 'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
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,
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';
@@ -10,9 +19,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikHasilKepuasan() { function EditGrafikHasilKepuasan() {
const editState = useProxy(grafikkepuasan) const editState = useProxy(grafikkepuasan);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nama: editState.update.form.nama || '', nama: editState.update.form.nama || '',
tanggal: editState.update.form.tanggal || '', tanggal: editState.update.form.tanggal || '',
@@ -22,12 +32,12 @@ function EditGrafikHasilKepuasan() {
}); });
useEffect(() => { useEffect(() => {
const loadKelahiran = async () => { const loadData = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
try { try {
const data = await editState.update.load(id); // akses langsung, bukan dari proxy const data = await editState.update.load(id);
if (data) { if (data) {
setFormData({ setFormData({
nama: data.nama || '', nama: data.nama || '',
@@ -43,21 +53,17 @@ function EditGrafikHasilKepuasan() {
} }
}; };
loadKelahiran(); loadData();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
editState.update.form = { editState.update.form = {
...editState.update.form, ...editState.update.form,
nama: formData.nama, ...formData,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
penyakit: formData.penyakit,
}; };
await editState.update.submit(); await editState.update.submit();
toast.success('grafik hasil kepuasan berhasil diperbarui!'); toast.success('Grafik hasil kepuasan berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan'); router.push('/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan');
} catch (error) { } catch (error) {
console.error('Error updating grafik hasil kepuasan:', error); console.error('Error updating grafik hasil kepuasan:', error);
@@ -66,47 +72,87 @@ function EditGrafikHasilKepuasan() {
}; };
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={30} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> onClick={() => router.back()}
<Stack gap={"xs"}> p="xs"
<Title order={3}>Edit grafik hasil kepuasan</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Grafik Hasil Kepuasan
</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
value={formData.nama} value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} label="Nama"
placeholder="masukkan nama" placeholder="Masukkan nama"
required
/> />
<TextInput <TextInput
type='date' type="date"
value={formData.tanggal} value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })} onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>} label="Tanggal"
placeholder="masukkan tanggal" placeholder="Masukkan tanggal"
required
/> />
<TextInput <TextInput
value={formData.jenisKelamin} value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })} onChange={(e) =>
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} setFormData({ ...formData, jenisKelamin: e.target.value })
placeholder="masukkan jenis kelamin" }
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/> />
<TextInput <TextInput
value={formData.alamat} value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" placeholder="Masukkan alamat"
required
/> />
<TextInput <TextInput
value={formData.penyakit} value={formData.penyakit}
onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })} onChange={(e) => setFormData({ ...formData, penyakit: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Penyakit</Text>} label="Penyakit"
placeholder="masukkan penyakit" placeholder="Masukkan penyakit"
required
/> />
<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,9 +1,8 @@
'use client' 'use client'
import { useProxy } from 'valtio/utils'; 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';
@@ -11,103 +10,130 @@ import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirma
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
function DetailGrafikHasilKepuasan() { function DetailGrafikHasilKepuasan() {
const state = useProxy(grafikkepuasan) const state = useProxy(grafikkepuasan);
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(() => {
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/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan") router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
} }
} };
if (!state.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<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 Data Grafik Hasil Kepuasan</Text> Kembali
{state.findUnique.data ? ( </Button>
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fw={"bold"} fz={"lg"}>Nama</Text> withBorder
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text> w={{ base: "100%", md: "50%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"lg"}>Tanggal</Text> radius="md"
<Text fz={"lg"}> shadow="sm"
{new Date(state.findUnique.data?.tanggal).toLocaleDateString('id-ID', { >
day: '2-digit', <Stack gap="md">
month: 'long', <Text fz="2xl" fw="bold" c={colors['blue-button']}>
year: 'numeric' Detail Data Grafik Hasil Kepuasan
})} </Text>
</Text>
</Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Box> <Stack gap="sm">
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text> <Box>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text> <Text fz="lg" fw="bold">Nama</Text>
</Box> <Text fz="md" c="dimmed">{data.nama || '-'}</Text>
<Box> </Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text> <Box>
</Box> <Text fz="lg" fw="bold">Tanggal</Text>
<Box> <Text fz="md" c="dimmed">
<Text fw={"bold"} fz={"lg"}>Penyakit</Text> {new Date(data.tanggal).toLocaleDateString("id-ID", {
<Text fz={"lg"} >{state.findUnique.data?.penyakit}</Text> day: "2-digit",
</Box> month: "long",
<Flex gap={"xs"} mt={10}> year: "numeric",
})}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data.jenisKelamin || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Penyakit</Text>
<Text fz="md" c="dimmed">{data.penyakit || '-'}</Text>
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" 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"
> >
<IconX size={20} /> <IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (state.findUnique.data) { onClick={() =>
router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${state.findUnique.data.id}/edit`); router.push(
} `/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${data.id}/edit`
}} )
disabled={!state.findUnique.data} }
color={"green"} variant="light"
radius="md"
size="md"
> >
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Stack> </Group>
</Paper> </Stack>
) : null} </Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -116,10 +142,10 @@ function DetailGrafikHasilKepuasan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus data ini?' text="Apakah anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );
} }
export default DetailGrafikHasilKepuasan; export default DetailGrafikHasilKepuasan;

View File

@@ -3,7 +3,17 @@
'use client' 'use client'
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, 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 { useState } from 'react'; import { useState } from 'react';
@@ -12,7 +22,7 @@ import { useProxy } from 'valtio/utils';
function CreateGrafikHasilKepuasanMasyarakat() { function CreateGrafikHasilKepuasanMasyarakat() {
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateGrafikKepuasan.create.form = { stateGrafikKepuasan.create.form = {
@@ -21,82 +31,97 @@ function CreateGrafikHasilKepuasanMasyarakat() {
jenisKelamin: "", jenisKelamin: "",
alamat: "", alamat: "",
penyakit: "", penyakit: "",
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
await stateGrafikKepuasan.create.create(); await stateGrafikKepuasan.create.create();
resetForm(); resetForm();
router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan"); router.push("/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan");
} };
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 size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Box> onClick={() => router.back()}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> p="xs"
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> radius="md"
<Stack gap={"xs"}> >
<TextInput <IconArrowBack color={colors['blue-button']} size={24} />
label="Nama" </Button>
type="text" </Tooltip>
value={stateGrafikKepuasan.create.form.nama} <Title order={4} ml="sm" c="dark">
placeholder="Masukkan nama" Tambah Grafik Hasil Kepuasan Masyarakat
onChange={(val) => { </Title>
stateGrafikKepuasan.create.form.nama = val.currentTarget.value; </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
label="Nama"
placeholder="Masukkan nama"
value={stateGrafikKepuasan.create.form.nama}
onChange={(e) => (stateGrafikKepuasan.create.form.nama = e.target.value)}
required
/>
<TextInput
type="date"
label="Tanggal"
placeholder="Masukkan tanggal"
value={stateGrafikKepuasan.create.form.tanggal}
onChange={(e) => (stateGrafikKepuasan.create.form.tanggal = e.target.value)}
required
/>
<TextInput
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
value={stateGrafikKepuasan.create.form.jenisKelamin}
onChange={(e) => (stateGrafikKepuasan.create.form.jenisKelamin = e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={stateGrafikKepuasan.create.form.alamat}
onChange={(e) => (stateGrafikKepuasan.create.form.alamat = e.target.value)}
required
/>
<TextInput
label="Penyakit"
placeholder="Masukkan penyakit"
value={stateGrafikKepuasan.create.form.penyakit}
onChange={(e) => (stateGrafikKepuasan.create.form.penyakit = 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)',
}} }}
/> >
<TextInput Simpan
label="Tanggal" </Button>
type="date" </Group>
value={stateGrafikKepuasan.create.form.tanggal} </Stack>
placeholder="Masukkan tanggal" </Paper>
onChange={(val) => {
stateGrafikKepuasan.create.form.tanggal = val.currentTarget.value;
}}
/>
<TextInput
label="Jenis Kelamin"
type="text"
value={stateGrafikKepuasan.create.form.jenisKelamin}
placeholder="Masukkan jenis kelamin"
onChange={(val) => {
stateGrafikKepuasan.create.form.jenisKelamin = val.currentTarget.value;
}}
/>
<TextInput
label="Alamat"
type="text"
value={stateGrafikKepuasan.create.form.alamat}
placeholder="Masukkan alamat"
onChange={(val) => {
stateGrafikKepuasan.create.form.alamat = val.currentTarget.value;
}}
/>
<TextInput
label="Penyakit"
type="text"
value={stateGrafikKepuasan.create.form.penyakit}
placeholder="Masukkan penyakit"
onChange={(val) => {
stateGrafikKepuasan.create.form.penyakit = val.currentTarget.value;
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,99 +1,108 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* 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, 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 { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } 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 { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Tooltip as ChartTooltip, Legend, XAxis, YAxis } from 'recharts';
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 grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
function GrafikHasilKepuasanMasyarakat() { function GrafikHasilKepuasanMasyarakat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter();
return ( return (
<Box> <Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Grafik Hasil Kepuasan Masyarakat' title='Grafik Hasil Kepuasan Masyarakat'
placeholder='pencarian' placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikHasilKepuasanMasyarakat search={search} /> <ListGrafikHasilKepuasanMasyarakat search={search} />
</Box> </Box>
); );
} }
function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) { function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
type PDKMGrafik = { type PDKMGrafik = {
id: string; id: string;
nama: string; nama: string;
tanggal: string | Date; // Allow both string and Date types tanggal: string | Date;
jenisKelamin: string; jenisKelamin: string;
alamat: string; alamat: string;
penyakit: string; penyakit: string;
createdAt?: Date; // Add optional fields that might come from the API };
updatedAt?: Date;
deletedAt?: Date | null;
}
const stateGrafikKepuasan = useProxy(grafikkepuasan); const stateGrafikKepuasan = useProxy(grafikkepuasan);
const [chartData, setChartData] = useState<PDKMGrafik[]>([]); const [chartData, setChartData] = useState<PDKMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)') const isTablet = useMediaQuery('(max-width: 1024px)');
const isMobile = useMediaQuery('(max-width: 768px)') const isMobile = useMediaQuery('(max-width: 768px)');
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateGrafikKepuasan.findMany;
data,
page,
totalPages,
loading,
load
} = stateGrafikKepuasan.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
load(page, 10, search) load(page, 10, search);
}, [page, search]) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true);
if (data) { if (data) {
setChartData(data.map((item) => ({ setChartData(data.map((item) => ({
id: item.id, ...item,
nama: item.nama, tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal
tanggal: item.tanggal instanceof Date ? item.tanggal.toISOString() : item.tanggal,
jenisKelamin: item.jenisKelamin,
alamat: item.alamat,
penyakit: item.penyakit,
}))); })));
} }
}, [data]); }, [data]);
// Add this function to process the data
const processDiseaseData = (data: PDKMGrafik[]) => { const processDiseaseData = (data: PDKMGrafik[]) => {
const diseaseCount: Record<string, number> = {}; const diseaseCount: Record<string, number> = {};
data.forEach(item => { data.forEach(item => {
const penyakit = item.penyakit.trim(); const penyakit = item.penyakit.trim();
if (penyakit) { if (penyakit) {
diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1; diseaseCount[penyakit] = (diseaseCount[penyakit] || 0) + 1;
} }
}); });
return Object.entries(diseaseCount).map(([name, count]) => ({ name, count }));
return Object.entries(diseaseCount).map(([name, count]) => ({
name,
count
}));
}; };
// Add this state to store the processed chart data
const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]); const [diseaseChartData, setDiseaseChartData] = useState<{ name: string, count: number }[]>([]);
// Update the chart data when data changes
useEffect(() => { useEffect(() => {
if (data && data.length > 0) { if (data && data.length > 0) {
setDiseaseChartData(processDiseaseData(data)); setDiseaseChartData(processDiseaseData(data));
@@ -104,97 +113,136 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
{/* Form Input */} {/* Judul + Tombol Tambah */}
<Paper bg={colors['white-1']} p={'md'}> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Grafik Hasil Kepuasan Masyarakat</Title>
title='List Grafik Hasil Kepuasan Masyarakat' <Tooltip label="Tambah Data" withArrow>
href='/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create' <Button
/> leftSection={<IconPlus size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() =>
router.push(
'/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/create'
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh> <TableTh>Tanggal</TableTh>
<TableTh>Jenis Kelamin</TableTh> <TableTh>Jenis Kelamin</TableTh>
<TableTh>Penyakit</TableTh> <TableTh>Penyakit</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.nama}</TableTd> <TableTr key={item.id}>
<TableTd> <TableTd>{item.nama}</TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', { <TableTd>
day: '2-digit', {new Date(item.tanggal).toLocaleDateString('id-ID', {
month: 'long', day: '2-digit',
year: 'numeric' month: 'long',
})} year: 'numeric',
</TableTd> })}
<TableTd>{item.jenisKelamin}</TableTd> </TableTd>
<TableTd>{item.penyakit}</TableTd> <TableTd>{item.jenisKelamin}</TableTd>
<TableTd> <TableTd>{item.penyakit}</TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`)}> <TableTd>
<IconDeviceImacCog size={20} /> <Button
</Button> variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/grafik_hasil_kepuasan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data kepuasan masyarakat yang cocok
</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box>
</Paper>
{/* Pagination */}
<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>
{/* Chart */}
<Box mt="lg" style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 ? (
<BarChart
width={isMobile ? 450 : isTablet ? 500 : 550}
height={350}
data={diseaseChartData}
>
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis />
<ChartTooltip />
<Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Paper> </Paper>
<Center> </Box>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 420, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Hasil Kepuasan Masyarakat</Title>
{mounted && diseaseChartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={diseaseChartData} >
<XAxis
dataKey="name"
tick={{ fontSize: 12 }}
interval={0}
angle={-45}
textAnchor="end"
height={70}
/>
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill={colors['blue-button']} name="Jumlah Kasus" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
</Box> </Box>
); );
} }

View File

@@ -4,7 +4,17 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
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';
@@ -128,33 +138,14 @@ function EditJadwalKegiatan() {
stateJadwalKegiatan.edit.form = { stateJadwalKegiatan.edit.form = {
...stateJadwalKegiatan.edit.form, ...stateJadwalKegiatan.edit.form,
content: formData.content, content: formData.content,
informasiJadwalKegiatan: { informasiJadwalKegiatan: { ...formData.informasiJadwalKegiatan },
name: formData.informasiJadwalKegiatan.name, deskripsiJadwalKegiatan: { ...formData.deskripsiJadwalKegiatan },
tanggal: formData.informasiJadwalKegiatan.tanggal, layananJadwalKegiatan: { ...formData.layananJadwalKegiatan },
waktu: formData.informasiJadwalKegiatan.waktu, syaratKetentuanJadwalKegiatan: { ...formData.syaratKetentuanJadwalKegiatan },
lokasi: formData.informasiJadwalKegiatan.lokasi, dokumenJadwalKegiatan: { ...formData.dokumenJadwalKegiatan },
}, pendaftaranJadwalKegiatan: { ...formData.pendaftaranJadwalKegiatan },
deskripsiJadwalKegiatan: {
deskripsi: formData.deskripsiJadwalKegiatan.deskripsi,
},
layananJadwalKegiatan: {
content: formData.layananJadwalKegiatan.content,
},
syaratKetentuanJadwalKegiatan: {
content: formData.syaratKetentuanJadwalKegiatan.content,
},
dokumenJadwalKegiatan: {
content: formData.dokumenJadwalKegiatan.content,
},
pendaftaranJadwalKegiatan: {
name: formData.pendaftaranJadwalKegiatan.name,
tanggal: formData.pendaftaranJadwalKegiatan.tanggal,
namaOrangtua: formData.pendaftaranJadwalKegiatan.namaOrangtua,
nomor: formData.pendaftaranJadwalKegiatan.nomor,
alamat: formData.pendaftaranJadwalKegiatan.alamat,
catatan: formData.pendaftaranJadwalKegiatan.catatan,
},
}; };
const success = await stateJadwalKegiatan.edit.submit(); const success = await stateJadwalKegiatan.edit.submit();
if (success) { if (success) {
toast.success("Jadwal kegiatan berhasil diperbarui!"); toast.success("Jadwal kegiatan berhasil diperbarui!");
@@ -165,241 +156,164 @@ function EditJadwalKegiatan() {
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan"); toast.error(error instanceof Error ? error.message : "Gagal memperbarui data jadwal kegiatan");
} }
}; };
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} />
<Stack gap="xs"> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Jadwal Kegiatan</Title> Edit Jadwal Kegiatan
</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">
{/* Nama Jadwal */}
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Jadwal Kegiatan</Text>} label="Nama Jadwal Kegiatan"
placeholder="masukkan nama jadwal kegiatan" placeholder="Masukkan nama jadwal kegiatan"
value={formData.content} value={formData.content}
onChange={(e) => { onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))}
setFormData(prev => ({
...prev,
content: e.target.value
}));
}}
/> />
<Box>
<Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text> {/* Deskripsi */}
<EditEditor
value={formData.deskripsiJadwalKegiatan.deskripsi}
onChange={(e) => {
setFormData(prev => ({
...prev,
deskripsiJadwalKegiatan: {
...prev.deskripsiJadwalKegiatan,
deskripsi: e
}
}));
}}
/>
</Box>
<Box> <Box>
<Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text> <Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text>
<TextInput <EditEditor
label={<Text fz="sm" fw="bold">Nama</Text>} value={formData.deskripsiJadwalKegiatan.deskripsi}
placeholder="masukkan nama" onChange={(val) => setFormData((prev) => ({
value={formData.informasiJadwalKegiatan.name} ...prev,
onChange={(e) => { deskripsiJadwalKegiatan: { deskripsi: val }
setFormData(prev => ({ }))}
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
name: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Tanggal</Text>}
placeholder="masukkan tanggal"
value={formData.informasiJadwalKegiatan.tanggal}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
tanggal: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Waktu</Text>}
placeholder="masukkan waktu"
value={formData.informasiJadwalKegiatan.waktu}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
waktu: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Lokasi</Text>}
placeholder="masukkan lokasi"
value={formData.informasiJadwalKegiatan.lokasi}
onChange={(e) => {
setFormData(prev => ({
...prev,
informasiJadwalKegiatan: {
...prev.informasiJadwalKegiatan,
lokasi: e.target.value
}
}));
}}
/> />
</Box> </Box>
{/* Informasi Jadwal */}
<Box>
<Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text>
<TextInput label="Nama" value={formData.informasiJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput type="date" label="Tanggal" value={formData.informasiJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput label="Waktu" value={formData.informasiJadwalKegiatan.waktu}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, waktu: e.target.value }
}))}
/>
<TextInput label="Lokasi" value={formData.informasiJadwalKegiatan.lokasi}
onChange={(e) => setFormData((prev) => ({
...prev, informasiJadwalKegiatan: { ...prev.informasiJadwalKegiatan, lokasi: e.target.value }
}))}
/>
</Box>
{/* Layanan */}
<Box> <Box>
<Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text>
<EditEditor <EditEditor
value={formData.layananJadwalKegiatan.content} value={formData.layananJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({ ...prev,
...prev, layananJadwalKegiatan: { content: val }
layananJadwalKegiatan: { }))}
...prev.layananJadwalKegiatan,
content: e
}
}));
}}
/> />
</Box> </Box>
{/* Syarat */}
<Box> <Box>
<Text fz="md" fw="bold">Syarat dan Ketentuan Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Syarat dan Ketentuan</Text>
<EditEditor <EditEditor
value={formData.syaratKetentuanJadwalKegiatan.content} value={formData.syaratKetentuanJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({ ...prev,
...prev, syaratKetentuanJadwalKegiatan: { content: val }
syaratKetentuanJadwalKegiatan: { }))}
...prev.syaratKetentuanJadwalKegiatan,
content: e
}
}));
}}
/> />
</Box> </Box>
{/* Dokumen */}
<Box> <Box>
<Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text> <Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text>
<EditEditor <EditEditor
value={formData.dokumenJadwalKegiatan.content} value={formData.dokumenJadwalKegiatan.content}
onChange={(e) => { onChange={(val) => setFormData((prev) => ({
setFormData(prev => ({ ...prev,
...prev, dokumenJadwalKegiatan: { content: val }
dokumenJadwalKegiatan: { }))}
...prev.dokumenJadwalKegiatan,
content: e
}
}));
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput
label={<Text fz="sm" fw="bold">Nama</Text>}
placeholder="masukkan nama"
value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
name: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Tanggal</Text>}
placeholder="masukkan tanggal"
value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
tanggal: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Nama Orangtua</Text>}
placeholder="masukkan nama orangtua"
value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
namaOrangtua: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Nomor</Text>}
placeholder="masukkan nomor"
value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
nomor: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>}
placeholder="masukkan alamat"
value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
alamat: e.target.value
}
}));
}}
/>
<TextInput
label={<Text fz="sm" fw="bold">Catatan</Text>}
placeholder="masukkan catatan"
value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => {
setFormData(prev => ({
...prev,
pendaftaranJadwalKegiatan: {
...prev.pendaftaranJadwalKegiatan,
catatan: e.target.value
}
}));
}}
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Pendaftaran */}
Simpan <Box>
</Button> <Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text>
<TextInput label="Nama" value={formData.pendaftaranJadwalKegiatan.name}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, name: e.target.value }
}))}
/>
<TextInput type="date" label="Tanggal" value={formData.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, tanggal: e.target.value }
}))}
/>
<TextInput label="Nama Orangtua" value={formData.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, namaOrangtua: e.target.value }
}))}
/>
<TextInput label="Nomor" value={formData.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, nomor: e.target.value }
}))}
/>
<TextInput label="Alamat" value={formData.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, alamat: e.target.value }
}))}
/>
<TextInput label="Catatan" value={formData.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => setFormData((prev) => ({
...prev, pendaftaranJadwalKegiatan: { ...prev.pendaftaranJadwalKegiatan, catatan: e.target.value }
}))}
/>
</Box>
{/* 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>
</Stack>
</Box> </Box>
); );
} }

View File

@@ -2,9 +2,9 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
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,83 +32,128 @@ function DetailJadwalKegiatan() {
if (!stateJadwalKegiatan.findUnique.data) { if (!stateJadwalKegiatan.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) )
} }
const data = stateJadwalKegiatan.findUnique.data
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Jadwal Kegiatan</Text> Kembali
{stateJadwalKegiatan.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fz={"lg"} fw={"bold"}>Nama Kegiatan</Text> withBorder
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.content}</Text> w={{ base: "100%", md: "50%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"lg"} fw={"bold"}>Informasi</Text> radius="md"
<Text fz={"md"} fw={"bold"}>Nama</Text> shadow="sm"
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.informasijadwalkegiatan.name}</Text> >
<Text fz={"md"} fw={"bold"}>Tanggal</Text> <Stack gap="md">
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.informasijadwalkegiatan.tanggal}</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"md"} fw={"bold"}>Waktu</Text> Detail Jadwal Kegiatan
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.informasijadwalkegiatan.waktu}</Text> </Text>
<Text fz={"md"} fw={"bold"}>Lokasi</Text>
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.informasijadwalkegiatan.lokasi}</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
</Box> <Stack gap="sm">
<Box> {/* Nama Kegiatan */}
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Box>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateJadwalKegiatan.findUnique.data.deskripsijadwalkegiatan.deskripsi }} /> <Text fz="lg" fw="bold">Nama Kegiatan</Text>
</Box> <Text fz="md" c="dimmed">{data.content || '-'}</Text>
<Box> </Box>
<Text fz={"lg"} fw={"bold"}>Layanan</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateJadwalKegiatan.findUnique.data.layananjadwalkegiatan.content }} /> {/* Informasi */}
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Informasi</Text>
<Text fz={"lg"} fw={"bold"}>Syarat Ketentuan</Text> <Text fz="md" fw="bold">Nama</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateJadwalKegiatan.findUnique.data.syaratketentuanjadwalkegiatan.content}} /> <Text fz="md" c="dimmed">{data.informasijadwalkegiatan.name || '-'}</Text>
</Box> <Text fz="md" fw="bold">Tanggal</Text>
<Box> <Text fz="md" c="dimmed">{data.informasijadwalkegiatan.tanggal || '-'}</Text>
<Text fz={"lg"} fw={"bold"}>Dokumen</Text> <Text fz="md" fw="bold">Waktu</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateJadwalKegiatan.findUnique.data.dokumenjadwalkegiatan.content }} /> <Text fz="md" c="dimmed">{data.informasijadwalkegiatan.waktu || '-'}</Text>
</Box> <Text fz="md" fw="bold">Lokasi</Text>
<Box> <Text fz="md" c="dimmed">{data.informasijadwalkegiatan.lokasi || '-'}</Text>
<Text fz={"lg"} fw={"bold"}>Prosedur Pendaftaran</Text> </Box>
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.name}</Text>
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.tanggal}</Text> {/* Deskripsi */}
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.namaOrangtua}</Text> <Box>
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.nomor}</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"md"}>{stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.alamat}</Text> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.deskripsijadwalkegiatan.deskripsi }} />
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: stateJadwalKegiatan.findUnique.data.pendaftaranjadwalkegiatan.catatan }} /> </Box>
</Box>
<Box> {/* Layanan */}
<Flex gap={"xs"}> <Box>
<Button color="red" onClick={() => { <Text fz="lg" fw="bold">Layanan</Text>
if (stateJadwalKegiatan.findUnique.data) { <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.layananjadwalkegiatan.content }} />
setSelectedId(stateJadwalKegiatan.findUnique.data.id) </Box>
setModalHapus(true)
} {/* Syarat Ketentuan */}
}}> <Box>
<IconX size={20} /> <Text fz="lg" fw="bold">Syarat Ketentuan</Text>
</Button> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.syaratketentuanjadwalkegiatan.content }} />
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${stateJadwalKegiatan.findUnique.data?.id}/edit`)} color="green"> </Box>
<IconEdit size={20} />
</Button> {/* Dokumen */}
</Flex> <Box>
</Box> <Text fz="lg" fw="bold">Dokumen</Text>
</Stack> <Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.dokumenjadwalkegiatan.content }} />
</Paper> </Box>
) : null}
{/* Prosedur Pendaftaran */}
<Box>
<Text fz="lg" fw="bold">Prosedur Pendaftaran</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.name}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.tanggal}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.namaOrangtua}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.nomor}</Text>
<Text fz="md" c="dimmed">{data.pendaftaranjadwalkegiatan.alamat}</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.pendaftaranjadwalkegiatan.catatan }} />
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id)
setModalHapus(true)
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -2,127 +2,158 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import jadwalKegiatanState from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
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 { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateJadwalKegiatan() { function CreateJadwalKegiatan() {
const stateJadwalKegiatan = useProxy(jadwalKegiatanState) const stateJadwalKegiatan = useProxy(jadwalKegiatanState);
const router = useRouter(); const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateJadwalKegiatan.edit.form = { stateJadwalKegiatan.create.form = {
content: "", content: '',
informasiJadwalKegiatan: { informasiJadwalKegiatan: {
name: "", name: '',
tanggal: "", tanggal: '',
waktu: "", waktu: '',
lokasi: "", lokasi: '',
}, },
deskripsiJadwalKegiatan: { deskripsiJadwalKegiatan: {
deskripsi: "", deskripsi: '',
}, },
layananJadwalKegiatan: { layananJadwalKegiatan: {
content: "", content: '',
}, },
syaratKetentuanJadwalKegiatan: { syaratKetentuanJadwalKegiatan: {
content: "", content: '',
}, },
dokumenJadwalKegiatan: { dokumenJadwalKegiatan: {
content: "", content: '',
}, },
pendaftaranJadwalKegiatan: { pendaftaranJadwalKegiatan: {
name: "", name: '',
tanggal: "", tanggal: '',
namaOrangtua: "", namaOrangtua: '',
nomor: "", nomor: '',
alamat: "", alamat: '',
catatan: "", catatan: '',
}, },
}; };
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
await stateJadwalKegiatan.create.submit(); await stateJadwalKegiatan.create.submit();
toast.success("Data berhasil disimpan"); toast.success('Data berhasil disimpan');
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan'); router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<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
</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">
Tambah Jadwal Kegiatan
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Jadwal Kegiatan</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
label={<Text fz="sm" fw="bold">Nama Jadwal Kegiatan</Text>} label="Nama Jadwal Kegiatan"
placeholder="masukkan nama jadwal kegiatan" placeholder="Masukkan nama jadwal kegiatan"
value={stateJadwalKegiatan.create.form.content} value={stateJadwalKegiatan.create.form.content}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.content = e.target.value; stateJadwalKegiatan.create.form.content = e.target.value;
}} }}
required
/> />
<Box>
<Text fz="sm" fw="bold">Deskripsi Jadwal Kegiatan</Text>
<CreateEditor
value={stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi}
onChange={(e) => {
stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi = e;
}}
/>
</Box>
<Box> <Box>
<Text fz="md" fw="bold">Informasi Jadwal Kegiatan</Text> <Text fz="sm" fw="bold" mb={4}>Deskripsi Jadwal Kegiatan</Text>
<CreateEditor
value={stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi}
onChange={(e) => {
stateJadwalKegiatan.create.form.deskripsiJadwalKegiatan.deskripsi = e;
}}
/>
</Box>
<Box>
<Text fz="md" fw="bold" mb="sm">Informasi Jadwal Kegiatan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama</Text>} label="Nama"
placeholder="masukkan nama" required
placeholder="Masukkan nama"
value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name} value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name = e.target.value; stateJadwalKegiatan.create.form.informasiJadwalKegiatan.name = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Tanggal</Text>} type="date"
placeholder="masukkan tanggal" required
label="Tanggal"
value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal} value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal = e.target.value; stateJadwalKegiatan.create.form.informasiJadwalKegiatan.tanggal = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Waktu</Text>} label="Waktu"
placeholder="masukkan waktu" required
placeholder="Masukkan waktu"
value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu} value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu = e.target.value; stateJadwalKegiatan.create.form.informasiJadwalKegiatan.waktu = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Lokasi</Text>} label="Lokasi"
placeholder="masukkan lokasi" required
placeholder="Masukkan lokasi"
value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi} value={stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi = e.target.value; stateJadwalKegiatan.create.form.informasiJadwalKegiatan.lokasi = e.target.value;
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz="md" fw="bold">Layanan Jadwal Kegiatan</Text> <Text fz="md" fw="bold" mb="sm">Layanan Jadwal Kegiatan</Text>
<CreateEditor <CreateEditor
value={stateJadwalKegiatan.create.form.layananJadwalKegiatan.content} value={stateJadwalKegiatan.create.form.layananJadwalKegiatan.content}
onChange={(e) => { onChange={(e) => {
@@ -130,8 +161,9 @@ function CreateJadwalKegiatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz="md" fw="bold">Syarat dan Ketentuan Jadwal Kegiatan</Text> <Text fz="md" fw="bold" mb="sm">Syarat & Ketentuan</Text>
<CreateEditor <CreateEditor
value={stateJadwalKegiatan.create.form.syaratKetentuanJadwalKegiatan.content} value={stateJadwalKegiatan.create.form.syaratKetentuanJadwalKegiatan.content}
onChange={(e) => { onChange={(e) => {
@@ -139,8 +171,9 @@ function CreateJadwalKegiatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz="md" fw="bold">Dokumen Jadwal Kegiatan</Text> <Text fz="md" fw="bold" mb="sm">Dokumen</Text>
<CreateEditor <CreateEditor
value={stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content} value={stateJadwalKegiatan.create.form.dokumenJadwalKegiatan.content}
onChange={(e) => { onChange={(e) => {
@@ -148,51 +181,58 @@ function CreateJadwalKegiatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz="md" fw="bold">Pendaftaran Jadwal Kegiatan</Text> <Text fz="md" fw="bold" mb="sm">Pendaftaran Jadwal Kegiatan</Text>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama</Text>} label="Nama"
placeholder="masukkan nama" required
placeholder="Masukkan nama"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.name = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Tanggal</Text>} type="date"
placeholder="masukkan tanggal" required
label="Tanggal"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.tanggal = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nama Orangtua</Text>} label="Nama Orangtua"
placeholder="masukkan nama orangtua" required
placeholder="Masukkan nama orangtua"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.namaOrangtua = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Nomor</Text>} label="Nomor"
placeholder="masukkan nomor" required
placeholder="Masukkan nomor"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.nomor = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" required
placeholder="Masukkan alamat"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.alamat = e.target.value;
}} }}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Catatan</Text>} label="Catatan"
placeholder="masukkan catatan" required
placeholder="Masukkan catatan"
value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan} value={stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan}
onChange={(e) => { onChange={(e) => {
stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan = e.target.value; stateJadwalKegiatan.create.form.pendaftaranJadwalKegiatan.catatan = e.target.value;
@@ -200,9 +240,21 @@ function CreateJadwalKegiatan() {
/> />
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}> {/* Save Button */}
Simpan <Group justify="right">
</Button> <Button
type="submit"
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,96 +1,185 @@
'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,
Paper,
Pagination,
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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header'; import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan'; import jadwalKegiatanState from '../../../_state/kesehatan/data_kesehatan_warga/jadwalKegiatan';
import { useState } from 'react'; import { useState } from 'react';
function JadwalKegiatan() { function JadwalKegiatan() {
const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Tombol Back */}
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors["blue-button"]} size={25} />
</Button>
</Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Jadwal Kegiatan' title="Jadwal Kegiatan"
placeholder='pencarian' placeholder="Cari nama, tanggal, lokasi..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListJadwalKegiatan search={search}/>
<ListJadwalKegiatan search={search} />
</Box> </Box>
); );
} }
function ListJadwalKegiatan({ search }: { search: string }) { function ListJadwalKegiatan({ search }: { search: string }) {
const stateJadwalKegiatan = useProxy(jadwalKegiatanState) const state = useProxy(jadwalKegiatanState);
const router = useRouter(); const router = useRouter();
const { data, page, totalPages, loading, load } = state.findMany;
useShallowEffect(() => { useShallowEffect(() => {
stateJadwalKegiatan.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
const filteredData = (stateJadwalKegiatan.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.informasijadwalkegiatan.name.toLowerCase().includes(keyword) || <Stack py={10}>
item.informasijadwalkegiatan.tanggal.toLowerCase().includes(keyword) || <Skeleton height={600} radius="md" />
item.informasijadwalkegiatan.waktu.toLowerCase().includes(keyword) || </Stack>
item.informasijadwalkegiatan.lokasi.toLowerCase().includes(keyword)
); );
});
if (!stateJadwalKegiatan.findMany.data) {
return (
<Box py={10}>
<Skeleton h={500}/>
</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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Jadwal Kegiatan' <Title order={4}>Daftar Jadwal Kegiatan</Title>
href='/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create' <Tooltip label="Tambah Jadwal Kegiatan" withArrow>
/> <Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
{/* Tabel */}
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama</TableTh> <TableTh>Nama</TableTh>
<TableTh>Tanggal</TableTh> <TableTh>Tanggal</TableTh>
<TableTh>Waktu</TableTh> <TableTh>Waktu</TableTh>
<TableTh>Lokasi</TableTh> <TableTh>Lokasi</TableTh>
<TableTh>Detail</TableTh> <TableTh>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.informasijadwalkegiatan.name}</TableTd> <TableTr key={item.id}>
<TableTd>{item.informasijadwalkegiatan.tanggal}</TableTd> <TableTd>
<TableTd>{item.informasijadwalkegiatan.waktu}</TableTd> <Text fw={500} truncate="end" lineClamp={1}>
<TableTd>{item.informasijadwalkegiatan.lokasi}</TableTd> {item.informasijadwalkegiatan.name}
<TableTd> </Text>
<Button onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${item.id}`)}> </TableTd>
<IconDeviceImacCog size={25} /> <TableTd>
</Button> {new Date(item.informasijadwalkegiatan.tanggal).toLocaleDateString(
'id-ID',
{
day: '2-digit',
month: 'long',
year: 'numeric',
}
)}
</TableTd>
<TableTd>{item.informasijadwalkegiatan.waktu}</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed">
{item.informasijadwalkegiatan.lokasi}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/jadwal_kegiatan/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada jadwal kegiatan yang cocok
</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Box> </Box>
</Stack> </Paper>
</Paper>
</Box> {/* Pagination */}
) <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>
);
} }
export default JadwalKegiatan; export default JadwalKegiatan;

View File

@@ -2,106 +2,162 @@
'use client' 'use client'
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
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,
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';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKelahiran() { function EditKelahiran() {
const editState = useProxy(persentaseKelahiranKematian.kelahiran) const editState = useProxy(persentaseKelahiranKematian.kelahiran);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
});
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try { const [formData, setFormData] = useState({
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy nama: editState.edit.form.nama || '',
if (data) { tanggal: editState.edit.form.tanggal || '',
setFormData({ jenisKelamin: editState.edit.form.jenisKelamin || '',
nama: data.nama || '', alamat: editState.edit.form.alamat || '',
tanggal: data.tanggal || '', });
jenisKelamin: data.jenisKelamin || '',
alamat: data.alamat || '',
});
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error("Gagal memuat data data kelahiran");
}
};
loadKelahiran();
}, [params?.id]);
const handleSubmit = async () => { useEffect(() => {
try { const loadKelahiran = async () => {
editState.edit.form = { const id = params?.id as string;
...editState.edit.form, if (!id) return;
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
};
await editState.edit.update();
toast.success('data kelahiran berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
return (
<Box> try {
<Box mb={10}> const data = await editState.edit.load(id);
<Button variant="subtle" onClick={() => router.back()}> if (data) {
<IconArrowBack color={colors["blue-button"]} size={30} /> setFormData({
</Button> nama: data.nama || '',
</Box> tanggal: data.tanggal || '',
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> jenisKelamin: data.jenisKelamin || '',
<Stack gap={"xs"}> alamat: data.alamat || '',
<Title order={3}>Edit data kelahiran</Title> });
<TextInput }
value={formData.nama} } catch (error) {
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} console.error('Error loading data kelahiran:', error);
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} toast.error('Gagal memuat data kelahiran');
placeholder="masukkan nama" }
/> };
<TextInput
type='date'
value={formData.tanggal} loadKelahiran();
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })} }, [params?.id]);
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>}
placeholder="masukkan tanggal"
/> const handleSubmit = async () => {
<TextInput try {
value={formData.jenisKelamin} editState.edit.form = {
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })} ...editState.edit.form,
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} nama: formData.nama,
placeholder="masukkan jenis kelamin" tanggal: formData.tanggal,
/> jenisKelamin: formData.jenisKelamin,
<TextInput alamat: formData.alamat,
value={formData.alamat} };
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>}
placeholder="masukkan alamat" await editState.edit.update();
/> toast.success('Data kelahiran berhasil diperbarui!');
<Button onClick={handleSubmit}>Simpan</Button> router.push(
</Stack> '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran'
</Paper> );
</Box> } catch (error) {
); console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<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 Data Kelahiran
</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
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
label="Nama"
placeholder="Masukkan nama"
required
/>
<TextInput
type="date"
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
label="Tanggal"
placeholder="Masukkan tanggal"
required
/>
<TextInput
value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
required
/>
<TextInput
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
label="Alamat"
placeholder="Masukkan alamat"
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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
} }
export default EditKelahiran;
export default EditKelahiran;

View File

@@ -1,11 +1,11 @@
'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 persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
@@ -13,109 +13,152 @@ import colors from '@/con/colors';
function DetailKelahiran() { function DetailKelahiran() {
const state = useProxy(persentaseKelahiranKematian.kelahiran) const state = useProxy(persentaseKelahiranKematian.kelahiran);
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(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => { useShallowEffect(() => {
if (selectedId) { state.findUnique.load(params?.id as string);
state.delete.byId(selectedId) }, []);
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return ( const handleHapus = () => {
<Box> if (selectedId) {
<Box mb={10}> state.delete.byId(selectedId);
<Button variant="subtle" onClick={() => router.back()}> setModalHapus(false);
<IconArrowBack color={colors['blue-button']} size={25} /> setSelectedId(null);
</Button> router.push(
</Box> "/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran"
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> );
<Stack> }
<Text fz={"xl"} fw={"bold"}>Detail Data Kelahiran</Text> };
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>
{new Date(state.findUnique.data?.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text>
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus if (!state.findUnique.data) {
opened={modalHapus} return (
onClose={() => setModalHapus(false)} <Stack py={10}>
onConfirm={handleHapus} <Skeleton height={500} radius="md" />
text='Apakah anda yakin ingin menghapus data ini?' </Stack>
/> );
</Box> }
);
const data = state.findUnique.data;
return (
<Box py={10}>
{/* Tombol Back */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Wrapper Detail */}
<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 Data Kelahiran
</Text>
<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">Tanggal</Text>
<Text fz="md" c="dimmed">
{new Date(data.tanggal).toLocaleDateString("id-ID", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data.jenisKelamin || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data.alamat || '-'}</Text>
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?"
/>
</Box>
);
} }
export default DetailKelahiran; export default DetailKelahiran;

View File

@@ -1,83 +1,126 @@
'use client' 'use client';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
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';
function CreateKelahiran() { function CreateKelahiran() {
const createState = useProxy(persentaseKelahiranKematian.kelahiran) const createState = useProxy(persentaseKelahiranKematian.kelahiran);
const router = useRouter(); const router = useRouter();
const resetForm = () => {
createState.create.form = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
};
};
const handleSubmit = async () => { const resetForm = () => {
await createState.create.create(); createState.create.form = {
resetForm(); nama: '',
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran") tanggal: '',
}; jenisKelamin: '',
alamat: '',
};
};
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'}> const handleSubmit = async () => {
<Stack gap={"xs"}> await createState.create.create();
<Title order={4}>Create Kelahiran</Title> resetForm();
<TextInput router.push(
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>} '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran'
placeholder='Masukkan nama' );
value={createState.create.form.nama} };
onChange={(val) => {
createState.create.form.nama = val.target.value;
}} return (
/> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<TextInput {/* Header */}
type='date' <Group mb="md">
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>} <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
placeholder='Masukkan tanggal' <Button
value={createState.create.form.tanggal} variant="subtle"
onChange={(val) => { onClick={() => router.back()}
createState.create.form.tanggal = val.target.value; p="xs"
}} radius="md"
/> >
<TextInput <IconArrowBack color={colors['blue-button']} size={24} />
label={<Text fw={"bold"} fz={"sm"}>Jenis Kelamin</Text>} </Button>
placeholder='Masukkan jenis kelamin' </Tooltip>
value={createState.create.form.jenisKelamin} <Title order={4} ml="sm" c="dark">
onChange={(val) => { Tambah Data Kelahiran
createState.create.form.jenisKelamin = val.target.value; </Title>
}} </Group>
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Alamat</Text>} {/* Form */}
placeholder='Masukkan alamat' <Paper
value={createState.create.form.alamat} w={{ base: '100%', md: '50%' }}
onChange={(val) => { bg={colors['white-1']}
createState.create.form.alamat = val.target.value; p="lg"
}} radius="md"
/> shadow="sm"
<Group> style={{ border: '1px solid #e0e0e0' }}
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> >
</Group> <Stack gap="md">
</Stack> <TextInput
</Paper> label={<Text fw="bold" fz="sm">Nama</Text>}
</Box> placeholder="Masukkan nama"
); value={createState.create.form.nama}
onChange={(e) => (createState.create.form.nama = e.target.value)}
required
/>
<TextInput
type="date"
label={<Text fw="bold" fz="sm">Tanggal</Text>}
placeholder="Masukkan tanggal"
value={createState.create.form.tanggal}
onChange={(e) => (createState.create.form.tanggal = e.target.value)}
required
/>
<TextInput
label={<Text fw="bold" fz="sm">Jenis Kelamin</Text>}
placeholder="Masukkan jenis kelamin"
value={createState.create.form.jenisKelamin}
onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)}
required
/>
<TextInput
label={<Text fw="bold" fz="sm">Alamat</Text>}
placeholder="Masukkan alamat"
value={createState.create.form.alamat}
onChange={(e) => (createState.create.form.alamat = 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)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
} }
export default CreateKelahiran;
export default CreateKelahiran;

View File

@@ -1,118 +1,197 @@
'use client' 'use client'
import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Pagination,
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 { IconArrowBack, IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconDeviceImacCog, 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';
function Kelahiran() { function Kelahiran() {
const router = useRouter(); const router = useRouter();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return (
<Box>
<Box mb={10}> return (
<Button variant="subtle" onClick={() => router.back()}> <Box>
<IconArrowBack color={colors["blue-button"]} size={30} /> {/* Tombol Back */}
</Button> <Box mb={10}>
</Box> <Button variant="subtle" onClick={() => router.back()}>
<HeaderSearch <IconArrowBack color={colors["blue-button"]} size={25} />
title='Data Kelahiran' </Button>
placeholder='pencarian' </Box>
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)} {/* Header Search */}
/> <HeaderSearch
<ListKelahiran search={search} /> title='Data Kelahiran'
</Box> placeholder='Cari nama atau alamat...'
); searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKelahiran search={search} />
</Box>
);
} }
function ListKelahiran({ search }: { search: string }) { function ListKelahiran({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kelahiran); const statePersentase = useProxy(persentasekelahiran.kelahiran);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = statePersentase.findMany;
data,
page,
totalPages,
loading,
load
} = statePersentase.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [page, search])
const filteredData = data || [] useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return ( const filteredData = data || [];
<Box>
<Stack gap={"xs"}>
{/* Form Input */} if (loading || !data) {
<Paper bg={colors['white-1']} p={'md'}> return (
<JudulList <Stack py={10}>
title='List Data Kelahiran' <Skeleton height={600} radius="md" />
href='/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create' </Stack>
/> );
<Table striped withTableBorder withRowBorders> }
<TableThead>
<TableTr>
<TableTh>Nama</TableTh> return (
<TableTh>Tanggal</TableTh> <Box py={10}>
<TableTh>Jenis Kelamin</TableTh> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<TableTh>Alamat</TableTh> {/* Judul + Tombol Tambah */}
<TableTh>Detail</TableTh> <Group justify="space-between" mb="md">
</TableTr> <Title order={4}>Daftar Data Kelahiran</Title>
</TableThead> <Tooltip label="Tambah Data Kelahiran" withArrow>
<TableTbody> <Button
{filteredData.map((item) => ( leftSection={<IconPlus size={18} />}
<TableTr key={item.id}> color="blue"
<TableTd>{item.nama}</TableTd> variant="light"
<TableTd> onClick={() =>
{new Date(item.tanggal).toLocaleDateString('id-ID', { router.push(
day: '2-digit', '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/create'
month: 'long', )
year: 'numeric' }
})} >
</TableTd> Tambah Baru
<TableTd>{item.jenisKelamin}</TableTd> </Button>
<TableTd>{item.alamat}</TableTd> </Tooltip>
<TableTd> </Group>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`)}>
<IconDeviceImacCog size={20} />
</Button> {/* Tabel */}
</TableTd> <Box style={{ overflowX: "auto" }}>
</TableTr> <Table highlightOnHover>
))} <TableThead>
</TableTbody> <TableTr>
</Table> <TableTh>Nama</TableTh>
</Paper> <TableTh>Tanggal</TableTh>
</Stack> <TableTh>Jenis Kelamin</TableTh>
<Center> <TableTh>Alamat</TableTh>
<Pagination <TableTh>Aksi</TableTh>
value={page} </TableTr>
onChange={(newPage) => load(newPage)} // ini penting! </TableThead>
total={totalPages} <TableTbody>
mt="md" {filteredData.length > 0 ? (
mb="md" filteredData.map((item) => (
/> <TableTr key={item.id}>
</Center> <TableTd>
</Box> <Text fw={500} truncate="end" lineClamp={1}>
); {item.nama}
</Text>
</TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>
<Text truncate fz="sm" c="dimmed">
{item.alamat}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`
)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data kelahiran yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<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>
);
} }
export default Kelahiran;
export default Kelahiran;

View File

@@ -3,119 +3,177 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
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';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKematian() { function EditKematian() {
const editState = useProxy(persentaseKelahiranKematian.kematian) const editState = useProxy(persentaseKelahiranKematian.kematian);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const [formData, setFormData] = useState({
nama: editState.edit.form.nama || '',
tanggal: editState.edit.form.tanggal || '',
jenisKelamin: editState.edit.form.jenisKelamin || '',
alamat: editState.edit.form.alamat || '',
penyebab: editState.edit.form.penyebab || '',
});
useEffect(() => {
const loadKelahiran = async () => {
const id = params?.id as string;
if (!id) return;
try { const [formData, setFormData] = useState({
const data = await editState.edit.load(id); // akses langsung, bukan dari proxy nama: editState.edit.form.nama || '',
if (data) { tanggal: editState.edit.form.tanggal || '',
setFormData({ jenisKelamin: editState.edit.form.jenisKelamin || '',
nama: data.nama || '', alamat: editState.edit.form.alamat || '',
tanggal: data.tanggal || '', penyebab: editState.edit.form.penyebab || '',
jenisKelamin: data.jenisKelamin || '', });
alamat: data.alamat || '',
penyebab: data.penyebab || '',
});
}
} catch (error) {
console.error("Error loading data kelahiran:", error);
toast.error("Gagal memuat data data kelahiran");
}
};
loadKelahiran();
}, [params?.id]);
const handleSubmit = async () => { useEffect(() => {
try { const loadData = async () => {
editState.edit.form = { const id = params?.id as string;
...editState.edit.form, if (!id) return;
nama: formData.nama,
tanggal: formData.tanggal,
jenisKelamin: formData.jenisKelamin,
alamat: formData.alamat,
penyebab: formData.penyebab,
};
await editState.edit.update();
toast.success('data kelahiran berhasil diperbarui!');
router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran');
} catch (error) {
console.error('Error updating data kelahiran:', error);
toast.error('Terjadi kesalahan saat memperbarui data kelahiran');
}
};
return (
<Box> try {
<Box mb={10}> const data = await editState.edit.load(id);
<Button variant="subtle" onClick={() => router.back()}> if (data) {
<IconArrowBack color={colors["blue-button"]} size={30} /> setFormData({
</Button> nama: data.nama || '',
</Box> tanggal: data.tanggal || '',
<Paper bg={"white"} p={"md"} w={{ base: "100%", md: "50%" }}> jenisKelamin: data.jenisKelamin || '',
<Stack gap={"xs"}> alamat: data.alamat || '',
<Title order={3}>Edit data kelahiran</Title> penyebab: data.penyebab || '',
<TextInput });
value={formData.nama} }
onChange={(e) => setFormData({ ...formData, nama: e.target.value })} } catch (error) {
label={<Text fz={"sm"} fw={"bold"}>Nama</Text>} console.error('Error loading data kematian:', error);
placeholder="masukkan nama" toast.error('Gagal memuat data kematian');
/> }
<TextInput };
type='date'
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })} loadData();
label={<Text fz={"sm"} fw={"bold"}>Tanggal</Text>} }, [params?.id]);
placeholder="masukkan tanggal"
/>
<TextInput const handleSubmit = async () => {
value={formData.jenisKelamin} try {
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })} editState.edit.form = { ...editState.edit.form, ...formData };
label={<Text fz={"sm"} fw={"bold"}>Jenis Kelamin</Text>} await editState.edit.update();
placeholder="masukkan jenis kelamin" toast.success('Data kematian berhasil diperbarui!');
/> router.push(
<TextInput '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
value={formData.alamat} );
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })} } catch (error) {
label={<Text fz={"sm"} fw={"bold"}>Alamat</Text>} console.error('Error updating data kematian:', error);
placeholder="masukkan alamat" toast.error('Terjadi kesalahan saat memperbarui data kematian');
/> }
<Box> };
<Text fz={"sm"} fw={"bold"}>Penyebab</Text>
<EditEditor
value={formData.penyebab} return (
onChange={(htmlContent) => { <Box px={{ base: 'sm', md: 'lg' }} py="md">
setFormData((prev) => ({ ...prev, penyebab: htmlContent })); {/* Header dengan tombol back */}
editState.edit.form.penyebab = htmlContent; <Group mb="md">
}} <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
/> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Button onClick={handleSubmit}>Simpan</Button> </Button>
</Stack> </Tooltip>
</Paper> <Title order={4} ml="sm" c="dark">
</Box> Edit Data Kematian
); </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
label="Nama"
placeholder="Masukkan nama"
value={formData.nama}
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
required
/>
<TextInput
type="date"
label="Tanggal"
placeholder="Masukkan tanggal"
value={formData.tanggal}
onChange={(e) => setFormData({ ...formData, tanggal: e.target.value })}
required
/>
<TextInput
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
value={formData.jenisKelamin}
onChange={(e) => setFormData({ ...formData, jenisKelamin: e.target.value })}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={formData.alamat}
onChange={(e) => setFormData({ ...formData, alamat: e.target.value })}
required
/>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Penyebab
</Text>
<EditEditor
value={formData.penyebab}
onChange={(htmlContent) => {
setFormData((prev) => ({ ...prev, penyebab: htmlContent }));
editState.edit.form.penyebab = htmlContent;
}}
/>
</Box>
<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>
</Paper>
</Box>
);
} }
export default EditKematian;
export default EditKematian;

View File

@@ -1,11 +1,11 @@
'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 persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
@@ -13,114 +13,153 @@ import colors from '@/con/colors';
function DetailKematian() { function DetailKematian() {
const state = useProxy(persentaseKelahiranKematian.kematian) const state = useProxy(persentaseKelahiranKematian.kematian);
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(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => { useShallowEffect(() => {
if (selectedId) { state.findUnique.load(params?.id as string);
state.delete.byId(selectedId) }, []);
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={40} />
</Stack>
)
}
return ( const handleHapus = () => {
<Box> if (selectedId) {
<Box mb={10}> state.delete.byId(selectedId);
<Button variant="subtle" onClick={() => router.back()}> setModalHapus(false);
<IconArrowBack color={colors['blue-button']} size={25} /> setSelectedId(null);
</Button> router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran");
</Box> }
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}> };
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Kematian</Text>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama</Text>
<Text fz={"lg"}>{state.findUnique.data?.nama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal</Text>
<Text fz={"lg"}>
{state.findUnique.data?.tanggal instanceof Date
? state.findUnique.data.tanggal.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})
: state.findUnique.data?.tanggal}
</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"} >{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat</Text>
<Text fz={"lg"} >{state.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Penyebab</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: state.findUnique.data?.penyebab || '' }} />
</Box>
<Flex gap={"xs"} mt={10}>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (state.findUnique.data) {
router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${state.findUnique.data.id}/edit`);
}
}}
disabled={!state.findUnique.data}
color={"green"}
>
<IconEdit size={20} />
</Button>
</Flex>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus if (!state.findUnique.data) {
opened={modalHapus} return (
onClose={() => setModalHapus(false)} <Stack py={10}>
onConfirm={handleHapus} <Skeleton height={500} radius="md" />
text='Apakah anda yakin ingin menghapus data ini?' </Stack>
/> );
</Box> }
);
const data = state.findUnique.data;
return (
<Box py={10}>
{/* Tombol kembali */}
<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 Data Kematian
</Text>
<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">Tanggal</Text>
<Text fz="md" c="dimmed">
{data?.tanggal instanceof Date
? data.tanggal.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})
: data?.tanggal || '-'}
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Jenis Kelamin</Text>
<Text fz="md" c="dimmed">{data?.jenisKelamin || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Alamat</Text>
<Text fz="md" c="dimmed">{data?.alamat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Penyebab</Text>
<Text fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data?.penyebab || '-' }} />
</Box>
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() => router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${data.id}/edit`
)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus data ini?"
/>
</Box>
);
} }
export default DetailKematian; export default DetailKematian;

View File

@@ -1,94 +1,146 @@
'use client' 'use client';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
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 { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateKematian() { function CreateKematian() {
const createState = useProxy(persentaseKelahiranKematian.kematian) const createState = useProxy(persentaseKelahiranKematian.kematian);
const router = useRouter(); const router = useRouter();
const resetForm = () => {
createState.create.form = {
nama: "",
tanggal: "",
jenisKelamin: "",
alamat: "",
penyebab: "",
};
};
const handleSubmit = async () => { const resetForm = () => {
await createState.create.create(); createState.create.form = {
resetForm(); nama: '',
router.push("/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian") tanggal: '',
}; jenisKelamin: '',
alamat: '',
penyebab: '',
};
};
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'}> const handleSubmit = async () => {
<Stack gap={"xs"}> if (!createState.create.form.nama) {
<Title order={4}>Create Kematian</Title> return toast.warn('Nama wajib diisi');
<TextInput }
label={<Text fw={"bold"} fz={"sm"}>Nama</Text>} if (!createState.create.form.tanggal) {
placeholder='Masukkan nama' return toast.warn('Tanggal wajib diisi');
value={createState.create.form.nama} }
onChange={(val) => {
createState.create.form.nama = val.target.value;
}} await createState.create.create();
/> resetForm();
<TextInput router.push(
type='date' '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian'
label={<Text fw={"bold"} fz={"sm"}>Tanggal</Text>} );
placeholder='Masukkan tanggal' };
value={createState.create.form.tanggal}
onChange={(val) => {
createState.create.form.tanggal = val.target.value; return (
}} <Box px={{ base: 'sm', md: 'lg' }} py="md">
/> {/* Header */}
<TextInput <Group mb="md">
label={<Text fw={"bold"} fz={"sm"}>Jenis Kelamin</Text>} <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
placeholder='Masukkan jenis kelamin' <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
value={createState.create.form.jenisKelamin} <IconArrowBack color={colors['blue-button']} size={24} />
onChange={(val) => { </Button>
createState.create.form.jenisKelamin = val.target.value; </Tooltip>
}} <Title order={4} ml="sm" c="dark">
/> Tambah Data Kematian
<TextInput </Title>
label={<Text fw={"bold"} fz={"sm"}>Alamat</Text>} </Group>
placeholder='Masukkan alamat'
value={createState.create.form.alamat}
onChange={(val) => { {/* Form Card */}
createState.create.form.alamat = val.target.value; <Paper
}} w={{ base: '100%', md: '50%' }}
/> bg={colors['white-1']}
<Box> p="lg"
<Text fw={"bold"} fz={"sm"}>Penyebab</Text> radius="md"
<CreateEditor shadow="sm"
value={createState.create.form.penyebab} style={{ border: '1px solid #e0e0e0' }}
onChange={(htmlContent) => { >
createState.create.form.penyebab = htmlContent; <Stack gap="md">
}} <TextInput
/> label="Nama"
</Box> placeholder="Masukkan nama"
<Group> value={createState.create.form.nama}
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> onChange={(e) => (createState.create.form.nama = e.target.value)}
</Group> required
</Stack> />
</Paper> <TextInput
</Box> type="date"
); label="Tanggal"
placeholder="Masukkan tanggal"
value={createState.create.form.tanggal}
onChange={(e) => (createState.create.form.tanggal = e.target.value)}
required
/>
<TextInput
label="Jenis Kelamin"
placeholder="Masukkan jenis kelamin"
value={createState.create.form.jenisKelamin}
onChange={(e) => (createState.create.form.jenisKelamin = e.target.value)}
required
/>
<TextInput
label="Alamat"
placeholder="Masukkan alamat"
value={createState.create.form.alamat}
onChange={(e) => (createState.create.form.alamat = e.target.value)}
required
/>
<Box>
<Title order={6} mb={6}>
Penyebab
</Title>
<CreateEditor
value={createState.create.form.penyebab}
onChange={(htmlContent) => {
createState.create.form.penyebab = htmlContent;
}}
/>
</Box>
<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>
</Paper>
</Box>
);
} }
export default CreateKematian;
export default CreateKematian;

View File

@@ -1,118 +1,191 @@
'use client' 'use client'
import HeaderSearch from '@/app/admin/(dashboard)/_com/header'; import HeaderSearch from '@/app/admin/(dashboard)/_com/header';
import JudulList from '@/app/admin/(dashboard)/_com/judulList';
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, 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 { IconArrowBack, IconEdit, IconSearch } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, 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';
function Kematian() { function Kematian() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const router = useRouter(); const router = useRouter();
return (
<Box>
<Box mb={10}> return (
<Button variant="subtle" onClick={() => router.back()}> <Box>
<IconArrowBack color={colors["blue-button"]} size={30} /> {/* Tombol Back */}
</Button> <Box mb={10}>
</Box> <Button variant="subtle" onClick={() => router.back()}>
<HeaderSearch <IconArrowBack color={colors["blue-button"]} size={30} />
title='Data Kematian' </Button>
placeholder='pencarian' </Box>
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)} {/* Header dengan Search */}
/> <HeaderSearch
<ListKematian search={search} /> title='Data Kematian'
</Box > placeholder='Cari nama atau alamat...'
); searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKematian search={search} />
</Box>
);
} }
function ListKematian({ search }: { search: string }) { function ListKematian({ search }: { search: string }) {
const statePersentase = useProxy(persentasekelahiran.kematian); const statePersentase = useProxy(persentasekelahiran.kematian);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = statePersentase.findMany;
data,
page,
totalPages,
loading,
load
} = statePersentase.findMany;
useShallowEffect(() => {
load(page, 10, search)
}, [search])
const filteredData = data || [] useShallowEffect(() => {
load(page, 10, search);
}, [page, search]);
if (loading || !data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return ( const filteredData = data || [];
<Box>
<Stack gap={"xs"}>
{/* Form Input */} if (loading || !data) {
<Paper bg={colors['white-1']} p={'md'}> return (
<JudulList <Stack py={10}>
title='List Data Kematian' <Skeleton height={600} radius="md" />
href='/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create' </Stack>
/> );
<Table striped withTableBorder withRowBorders> }
<TableThead>
<TableTr>
<TableTh>Nama</TableTh> return (
<TableTh>Tanggal</TableTh> <Box py={10}>
<TableTh>Jenis Kelamin</TableTh> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<TableTh>Alamat</TableTh> <Group justify="space-between" mb="md">
<TableTh>Detail</TableTh> <Title order={4}>Daftar Data Kematian</Title>
</TableTr> <Tooltip label="Tambah Data Kematian" withArrow>
</TableThead> <Button
<TableTbody> leftSection={<IconPlus size={18} />}
{filteredData.map((item) => ( color="blue"
<TableTr key={item.id}> variant="light"
<TableTd>{item.nama}</TableTd> onClick={() =>
<TableTd> router.push(
{new Date(item.tanggal).toLocaleDateString('id-ID', { '/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/create'
day: '2-digit', )
month: 'long', }
year: 'numeric' >
})} Tambah Baru
</TableTd> </Button>
<TableTd>{item.jenisKelamin}</TableTd> </Tooltip>
<TableTd>{item.alamat}</TableTd> </Group>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran/${item.id}`)}>
<IconEdit size={20} /> <Box style={{ overflowX: "auto" }}>
</Button> <Table highlightOnHover>
</TableTd> <TableThead>
</TableTr> <TableTr>
))} <TableTh>Nama</TableTh>
</TableTbody> <TableTh>Tanggal</TableTh>
</Table> <TableTh>Jenis Kelamin</TableTh>
</Paper> <TableTh>Alamat</TableTh>
</Stack> <TableTh>Aksi</TableTh>
<Center> </TableTr>
<Pagination </TableThead>
value={page} <TableTbody>
onChange={(newPage) => load(newPage)} // ini penting! {filteredData.length > 0 ? (
total={totalPages} filteredData.map((item) => (
mt="md" <TableTr key={item.id}>
mb="md" <TableTd>
/> <Text fw={500} truncate="end" lineClamp={1}>
</Center> {item.nama}
</Box> </Text>
); </TableTd>
<TableTd>
{new Date(item.tanggal).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
</TableTd>
<TableTd>{item.jenisKelamin}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() =>
router.push(
`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian/${item.id}`
)
}
>
<IconEdit size={18} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text color="dimmed">
Tidak ada data kematian yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
</Box>
);
} }
export default Kematian;
export default Kematian;

View File

@@ -1,228 +1,277 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran'; import persentasekelahiran from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { ActionIcon, Box, Center, Flex, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core'; import { ActionIcon, Badge, Box, Center, Flex, Tooltip as MantineTooltip, Paper, Select, Skeleton, Stack, Table, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconBabyCarriage, IconGrave2 } from '@tabler/icons-react'; import { IconBabyCarriage, IconGrave2 } 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 { Bar, BarChart, Legend, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
type TooltipPayload = { type TooltipPayload = {
name: string; name: string;
value: number; value: number;
payload: any; payload: any;
color: string; color: string;
dataKey: string; dataKey: string;
}; };
type CustomTooltipProps = TooltipProps<number, string> & { type CustomTooltipProps = TooltipProps<number, string> & {
active?: boolean; active?: boolean;
payload?: TooltipPayload[]; payload?: TooltipPayload[];
label?: string; label?: string;
}; };
function PersentaseDataKelahiranKematian() { function PersentaseDataKelahiranKematian() {
return ( return (
<Stack gap={"xs"}> <Stack gap="md">
<GrafikPersentaseKelahiranKematian /> <GrafikPersentaseKelahiranKematian />
</Stack> </Stack>
); );
} }
function GrafikPersentaseKelahiranKematian() { function GrafikPersentaseKelahiranKematian() {
const router = useRouter(); const router = useRouter();
type DataTahunan = {
tahun: string;
totalKelahiran: number;
totalKematian: number;
data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
// Count occurrences per year
const countByYear = (data: any[], dateField: string) => {
const counts: Record<string, number> = {};
data?.forEach(item => {
const year = new Date(item[dateField]).getFullYear().toString();
counts[year] = (counts[year] || 0) + 1;
});
return counts;
};
const statePersentase = useProxy(persentasekelahiran); type DataTahunan = {
const [chartData, setChartData] = useState<DataTahunan[]>([]); tahun: string;
const isTablet = useMediaQuery('(max-width: 1024px)'); totalKelahiran: number;
const isMobile = useMediaQuery('(max-width: 768px)'); totalKematian: number;
const [selectedYear, setSelectedYear] = useState<string | null>(null); data: Array<{
id: string;
bulan: string;
kelahiran: number;
kematian: number;
}>;
};
// Format number to Indonesian locale
const formatNumber = (num: number) => {
return new Intl.NumberFormat('id-ID').format(num);
};
// Format tooltip // ✅ Fungsi hitung tahunan + bulanan
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { const countByYearAndMonth = (kelahiran: any[], kematian: any[]): DataTahunan[] => {
if (active && payload && payload.length) { const dataTahunan: Record<string, DataTahunan> = {};
return (
<Paper p="md" shadow="md" withBorder>
<Text size="sm" fw={500} mb={5}>Tahun {label}</Text>
<Text size="sm" c="blue">Kelahiran: {formatNumber(payload[0].value)}</Text>
<Text size="sm" c="red">Kematian: {formatNumber(payload[1].value)}</Text>
</Paper>
);
}
return null;
};
useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000); // Load all kelahiran data
statePersentase.kematian.findMany.load(1, 1000); // Load all kematian data
}, []);
useEffect(() => { const namaBulan = [
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) { 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
// Count kelahiran and kematian by year 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
const kelahiranByYear = countByYear(statePersentase.kelahiran.findMany.data, 'tanggal'); ];
const kematianByYear = countByYear(statePersentase.kematian.findMany.data, 'tanggal');
// Get all unique years
const allYears = new Set([
...Object.keys(kelahiranByYear),
...Object.keys(kematianByYear)
]);
// Create data structure for the chart // Proses kelahiran
const dataByYear = Array.from(allYears).reduce<Record<string, DataTahunan>>((acc, year) => { kelahiran?.forEach((item: any) => {
acc[year] = { const date = new Date(item.tanggal);
tahun: year, const tahun = date.getFullYear().toString();
totalKelahiran: kelahiranByYear[year] || 0, const bulanIndex = date.getMonth();
totalKematian: kematianByYear[year] || 0,
data: []
};
return acc;
}, {});
const sortedData = Object.values(dataByYear).sort((a, b) =>
parseInt(a.tahun) - parseInt(b.tahun)
);
setChartData(sortedData); if (!dataTahunan[tahun]) {
setSelectedYear(sortedData[0]?.tahun || ''); dataTahunan[tahun] = {
} tahun,
}, [ totalKelahiran: 0,
statePersentase.kelahiran.findMany.data, totalKematian: 0,
statePersentase.kematian.findMany.data, data: namaBulan.map((nama, idx) => ({
]); id: `${tahun}-${idx + 1}`,
bulan: nama,
kelahiran: 0,
kematian: 0
}))
};
}
if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
);
}
const selectedYearData = chartData.find(d => d.tahun === selectedYear); dataTahunan[tahun].totalKelahiran += 1;
dataTahunan[tahun].data[bulanIndex].kelahiran += 1;
});
return (
<Paper bg={colors['white-1']} p="md">
<Stack gap={"xs"}>
<Title order={3} mb="md">Statistik Kelahiran & Kematian</Title>
<Box>
<Flex gap={"xs"}>
<Box>
<ActionIcon size={30} color={colors['blue-button']} onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
<IconBabyCarriage size={30} color={colors['white-1']} />
</ActionIcon>
</Box>
<Box>
<ActionIcon size={30} color={colors['blue-button']} onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')} >
<IconGrave2 size={30} color={colors['white-1']} />
</ActionIcon>
</Box>
</Flex>
</Box>
</Stack>
{chartData.length === 0 ? ( // Proses kematian
<Text c="dimmed" ta="center" py="xl"> kematian?.forEach((item: any) => {
Belum ada data yang tersedia untuk ditampilkan const date = new Date(item.tanggal);
</Text> const tahun = date.getFullYear().toString();
) : ( const bulanIndex = date.getMonth();
<>
{/* Year Selector */}
<Box mb="md" style={{ maxWidth: '200px' }}>
<Select
label="Pilih Tahun"
placeholder="Pilih Tahun"
data={chartData.map((item) => ({
value: item.tahun,
label: item.tahun
}))}
value={selectedYear}
onChange={(value) => setSelectedYear(value || '')}
size="xs"
/>
</Box>
{/* Main Chart */}
<Center>
<Box h={400}>
<BarChart
width={isMobile ? window.innerWidth * 0.9 : isTablet ? 700 : 800}
height={350}
data={chartData}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
>
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="totalKelahiran" name="Total Kelahiran" fill="#4dabf7" />
<Bar dataKey="totalKematian" name="Total Kematian" fill="#f03e3e" />
</BarChart>
</Box>
</Center>
{/* Yearly Breakdown */} if (!dataTahunan[tahun]) {
{selectedYearData && ( dataTahunan[tahun] = {
<Box mt="xl"> tahun,
<Title order={4} mb="md">Rincian Tahun {selectedYear}</Title> totalKelahiran: 0,
<Table striped withTableBorder> totalKematian: 0,
<Table.Thead> data: namaBulan.map((nama, idx) => ({
<Table.Tr> id: `${tahun}-${idx + 1}`,
<Table.Th>Bulan</Table.Th> bulan: nama,
<Table.Th ta="right">Kelahiran</Table.Th> kelahiran: 0,
<Table.Th ta="right">Kematian</Table.Th> kematian: 0
</Table.Tr> }))
</Table.Thead> };
<Table.Tbody> }
{selectedYearData.data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.bulan}</Table.Td> dataTahunan[tahun].totalKematian += 1;
<Table.Td ta="right">{formatNumber(item.kelahiran)}</Table.Td> dataTahunan[tahun].data[bulanIndex].kematian += 1;
<Table.Td ta="right">{formatNumber(item.kematian)}</Table.Td> });
</Table.Tr>
))}
<Table.Tr style={{ fontWeight: 'bold' }}> return Object.values(dataTahunan).sort((a, b) => parseInt(a.tahun) - parseInt(b.tahun));
<Table.Td>Total</Table.Td> };
<Table.Td ta="right">{formatNumber(selectedYearData.totalKelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKematian)}</Table.Td>
</Table.Tr> const statePersentase = useProxy(persentasekelahiran);
</Table.Tbody> const [chartData, setChartData] = useState<DataTahunan[]>([]);
</Table> const [selectedYear, setSelectedYear] = useState<string | null>(null);
</Box>
)}
</> const formatNumber = (num: number) => new Intl.NumberFormat('id-ID').format(num);
)}
</Paper>
); const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<Paper p="sm" shadow="md" withBorder radius="md">
<Text size="sm" fw={600}>Tahun {label}</Text>
<Text size="sm" c="blue.6">Kelahiran: {formatNumber(payload[0].value)}</Text>
<Text size="sm" c="red.6">Kematian: {formatNumber(payload[1].value)}</Text>
</Paper>
);
}
return null;
};
useShallowEffect(() => {
statePersentase.kelahiran.findMany.load(1, 1000);
statePersentase.kematian.findMany.load(1, 1000);
}, []);
useEffect(() => {
if (statePersentase.kelahiran.findMany.data && statePersentase.kematian.findMany.data) {
const hasil = countByYearAndMonth(
statePersentase.kelahiran.findMany.data,
statePersentase.kematian.findMany.data
);
setChartData(hasil);
setSelectedYear(hasil[0]?.tahun || null);
}
}, [statePersentase.kelahiran.findMany.data, statePersentase.kematian.findMany.data]);
if (!statePersentase.kelahiran.findMany.data || !statePersentase.kematian.findMany.data) {
return <Skeleton h={400} radius="lg" />;
}
const selectedYearData = chartData.find(d => d.tahun === selectedYear);
return (
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack gap="lg">
<Flex justify="space-between" align="center">
<Title order={3} fw={700}>Statistik Kelahiran & Kematian</Title>
<Flex gap="sm">
<MantineTooltip label="Tambah Data Kelahiran" withArrow>
<ActionIcon size="lg" radius="xl" color="blue.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kelahiran')}>
<IconBabyCarriage size={22} />
</ActionIcon>
</MantineTooltip>
<MantineTooltip label="Tambah Data Kematian" withArrow>
<ActionIcon size="lg" radius="xl" color="red.6" onClick={() => router.push('/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/kematian')}>
<IconGrave2 size={22} />
</ActionIcon>
</MantineTooltip>
</Flex>
</Flex>
{chartData.length === 0 ? (
<Center py="xl">
<Text c="dimmed" fs="italic">Belum ada data untuk ditampilkan</Text>
</Center>
) : (
<>
<Box maw={220}>
<Select
label="Pilih Tahun"
placeholder="Pilih tahun data"
data={chartData.map((item) => ({ value: item.tahun, label: item.tahun }))}
value={selectedYear}
onChange={(value) => setSelectedYear(value || null)}
size="sm"
radius="md"
/>
</Box>
<Box h={360}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData} margin={{ top: 20, right: 30, left: 0, bottom: 10 }}>
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend />
<Bar dataKey="totalKelahiran" name="Kelahiran" fill="#4dabf7" radius={[6, 6, 0, 0]} />
<Bar dataKey="totalKematian" name="Kematian" fill="#f03e3e" radius={[6, 6, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Box>
{selectedYearData && (
<Box>
<Flex align="center" gap="sm" mb="md">
<Title order={4} fw={600}>Rincian Tahun {selectedYear}</Title>
<Badge variant="light" color="blue">{formatNumber(selectedYearData.totalKelahiran)} kelahiran</Badge>
<Badge variant="light" color="red">{formatNumber(selectedYearData.totalKematian)} kematian</Badge>
</Flex>
<Table striped withTableBorder highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Bulan</Table.Th>
<Table.Th ta="right">Kelahiran</Table.Th>
<Table.Th ta="right">Kematian</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{selectedYearData.data.length > 0 ? (
<>
{selectedYearData.data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.bulan}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(item.kematian)}</Table.Td>
</Table.Tr>
))}
<Table.Tr style={{ fontWeight: 'bold' }}>
<Table.Td>Total</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKelahiran)}</Table.Td>
<Table.Td ta="right">{formatNumber(selectedYearData.totalKematian)}</Table.Td>
</Table.Tr>
</>
) : (
<Table.Tr>
<Table.Td colSpan={3} ta="center" c="dimmed">Tidak ada rincian bulanan</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Box>
)}
</>
)}
</Stack>
</Paper>
);
} }
export default PersentaseDataKelahiranKematian;
export default PersentaseDataKelahiranKematian;

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '@/app/admin/(dashboard)/_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
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';
@@ -12,11 +23,10 @@ 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 EditInfoWabahPenyakit() { function EditInfoWabahPenyakit() {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const infoWabahPenyakitState = useProxy(infoWabahPenyakit);
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);
@@ -25,7 +35,7 @@ function EditInfoWabahPenyakit() {
deskripsiSingkat: infoWabahPenyakitState.edit.form.deskripsiSingkat || '', deskripsiSingkat: infoWabahPenyakitState.edit.form.deskripsiSingkat || '',
deskripsi: infoWabahPenyakitState.edit.form.deskripsiLengkap || '', deskripsi: infoWabahPenyakitState.edit.form.deskripsiLengkap || '',
imageId: infoWabahPenyakitState.edit.form.imageId || '', imageId: infoWabahPenyakitState.edit.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const loadInfoWabahPenyakit = async () => { const loadInfoWabahPenyakit = async () => {
@@ -47,8 +57,8 @@ function EditInfoWabahPenyakit() {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program kesehatan:", error); console.error('Error loading info wabah penyakit:', error);
toast.error("Gagal memuat data program kesehatan"); toast.error('Gagal memuat data info wabah penyakit');
} }
}; };
@@ -70,115 +80,143 @@ function EditInfoWabahPenyakit() {
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');
} }
infoWabahPenyakitState.edit.form.imageId = uploaded.id; infoWabahPenyakitState.edit.form.imageId = uploaded.id;
} }
await infoWabahPenyakitState.edit.update(); await infoWabahPenyakitState.edit.update();
toast.success("Info wabah penyakit berhasil diperbarui!"); toast.success('Info wabah penyakit berhasil diperbarui!');
router.push("/admin/kesehatan/info-wabah-penyakit"); router.push('/admin/kesehatan/info-wabah-penyakit');
} catch (error) { } catch (error) {
console.error("Error updating info wabah penyakit:", error); console.error('Error updating info wabah penyakit:', error);
toast.error("Gagal memuat data info wabah penyakit"); toast.error('Terjadi kesalahan saat memperbarui info wabah penyakit');
} }
} };
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} />
<Stack gap={"xs"}> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Info Wabah Penyakit</Title> Edit Info Wabah Penyakit
<TextInput </Title>
value={formData.name} </Group>
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput {/* Form */}
value={formData.deskripsiSingkat} <Paper
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })} w={{ base: '100%', md: '50%' }}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>} bg={colors['white-1']}
placeholder="masukkan deskripsi" p="lg"
/> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label="Judul"
placeholder="Masukkan judul"
required
/>
<Box> <TextInput
<Text fz="sm" fw="bold">Deskripsi</Text> value={formData.deskripsiSingkat}
<EditEditor onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
value={formData.deskripsi} label="Deskripsi Singkat"
onChange={(val) => setFormData({ ...formData, deskripsi: val })} placeholder="Masukkan deskripsi singkat"
/> required
</Box> />
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Box>
<Box> <Text fz="sm" fw="bold">
<Dropzone Deskripsi
onDrop={(files) => { </Text>
const selectedFile = files[0]; // Ambil file pertama <EditEditor
if (selectedFile) { value={formData.deskripsi}
setFile(selectedFile); onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview />
} </Box>
<Box>
<Text fz="sm" fw="bold">
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={200} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
onReject={() => toast.error('File tidak valid.')} />
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> </Box>
</Box> )}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
<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 Simpan
</Button> </Button>
</Stack> </Group>
</Paper> </Stack>
</Stack> </Paper>
</Box > </Box>
); );
} }

View File

@@ -1,7 +1,17 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core'; import {
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; Box,
Button,
Paper,
Stack,
Text,
Skeleton,
Tooltip,
Group,
Image,
} from '@mantine/core';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit'; import infoWabahPenyakit from '../../../_state/kesehatan/info-wabah-penyakit/infoWabahPenyakit';
@@ -10,86 +20,135 @@ import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailInfoWabahPenyakit() { function DetailInfoWabahPenyakit() {
const infoWabahPenyakitState = useProxy(infoWabahPenyakit) const state = useProxy(infoWabahPenyakit);
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(() => {
infoWabahPenyakitState.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
infoWabahPenyakitState.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/info-wabah-penyakit") router.push('/admin/kesehatan/info-wabah-penyakit');
} }
} };
if (!infoWabahPenyakitState.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={400} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Info Wabah Penyakit</Text> Kembali
{infoWabahPenyakitState.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fz={"lg"} fw={"bold"}>Judul</Text> withBorder
<Text fz={"lg"}>{infoWabahPenyakitState.findUnique.data.name}</Text> w={{ base: '100%', md: '50%' }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text> radius="md"
<Text fz={"lg"}>{infoWabahPenyakitState.findUnique.data.deskripsiSingkat}</Text> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: infoWabahPenyakitState.findUnique.data.deskripsiLengkap }} /> Detail Info Wabah Penyakit
</Box> </Text>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Image src={infoWabahPenyakitState.findUnique.data.image?.link} alt="gambar" /> <Stack gap="sm">
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Judul</Text>
<Flex gap={"xs"}> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
<Button color="red" onClick={() => { </Box>
if (infoWabahPenyakitState.findUnique.data) {
setSelectedId(infoWabahPenyakitState.findUnique.data.id) <Box>
setModalHapus(true) <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
} <Text fz="md" c="dimmed">{data.deskripsiSingkat || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Deskripsi Lengkap</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsiLengkap }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt="gambar wabah"
radius="md"
mt="xs"
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}} }}
disabled={infoWabahPenyakitState.delete.loading || !infoWabahPenyakitState.findUnique.data} variant="light"
> radius="md"
<IconX size={20} /> size="md"
</Button> >
<Button onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${infoWabahPenyakitState.findUnique.data?.id}/edit`)} color="green"> <IconTrash size={20} />
<IconEdit size={20} /> </Button>
</Button> </Tooltip>
</Flex>
</Box> <Tooltip label="Edit Data" withArrow position="top">
</Stack> <Button
</Paper> color="green"
) : null} onClick={() =>
router.push(
`/admin/kesehatan/info-wabah-penyakit/${data.id}/edit`
)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

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';
@@ -18,15 +29,12 @@ function CreateInfoWabahPenyakit() {
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const resetForm = () => { const resetForm = () => {
// Reset state di valtio
infoWabahPenyakitState.create.form = { infoWabahPenyakitState.create.form = {
name: "", name: "",
deskripsiSingkat: "", deskripsiSingkat: "",
deskripsiLengkap: "", deskripsiLengkap: "",
imageId: "", imageId: "",
}; };
// Reset state lokal
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
@@ -36,7 +44,6 @@ function CreateInfoWabahPenyakit() {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn("Pilih file gambar terlebih dahulu");
} }
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -47,34 +54,50 @@ function CreateInfoWabahPenyakit() {
return toast.error("Gagal upload gambar"); return toast.error("Gagal upload gambar");
} }
// Simpan ID gambar ke form
infoWabahPenyakitState.create.form.imageId = uploaded.id; infoWabahPenyakitState.create.form.imageId = uploaded.id;
// Submit data berita
await infoWabahPenyakitState.create.create(); await infoWabahPenyakitState.create.create();
// Reset form setelah submit
resetForm(); resetForm();
router.push("/admin/kesehatan/info-wabah-penyakit") router.push("/admin/kesehatan/info-wabah-penyakit")
}; };
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
</Box> variant="subtle"
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> onClick={() => router.back()}
<Stack gap="xs"> p="xs"
<Title order={3}>Create Info Wabah Penyakit</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Info Wabah Penyakit
</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
value={infoWabahPenyakitState.create.form.name} value={infoWabahPenyakitState.create.form.name}
onChange={(val) => { onChange={(val) => {
infoWabahPenyakitState.create.form.name = val.target.value; infoWabahPenyakitState.create.form.name = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="Masukkan judul"
required
/> />
<TextInput <TextInput
@@ -83,7 +106,8 @@ function CreateInfoWabahPenyakit() {
infoWabahPenyakitState.create.form.deskripsiSingkat = val.target.value; infoWabahPenyakitState.create.form.deskripsiSingkat = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>} label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>}
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi singkat"
required
/> />
<Box> <Box>
@@ -95,65 +119,74 @@ function CreateInfoWabahPenyakit() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz="sm" fw="bold">Gambar</Text>
<Box> <Dropzone
<Dropzone onDrop={(files) => {
onDrop={(files) => { const selectedFile = files[0];
const selectedFile = files[0]; // Ambil file pertama if (selectedFile) {
if (selectedFile) { setFile(selectedFile);
setFile(selectedFile); setPreviewImage(URL.createObjectURL(selectedFile));
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview }
} }}
}} onReject={() => toast.error('File tidak valid.')}
onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2}
maxSize={5 * 1024 ** 2} // Maks 5MB accept={{ 'image/*': [] }}
accept={{ 'image/*': [] }} >
> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Dropzone.Accept>
<Dropzone.Accept> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={52} color="var(--mantine-color-blue-6)" 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={52} color="var(--mantine-color-red-6)" stroke={1.5} /> </Dropzone.Reject>
</Dropzone.Reject> <Dropzone.Idle>
<Dropzone.Idle> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> </Dropzone.Idle>
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih 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" inline mt={7}>
Maksimal 5MB dan harus format gambar Maksimal 5MB dan harus format gambar
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {previewImage && (
{previewImage && ( <Box mt="sm">
<Box mt="sm"> <Image
<Image src={previewImage}
src={previewImage} alt="Preview"
alt="Preview" style={{
style={{ maxWidth: '100%',
maxWidth: '100%', maxHeight: '200px',
maxHeight: '200px', objectFit: 'contain',
objectFit: 'contain', borderRadius: '8px',
borderRadius: '8px', border: '1px solid #ddd',
border: '1px solid #ddd', }}
}} />
/> </Box>
</Box> )}
)}
</Box>
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan <Group justify="right">
</Button> <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,26 @@
'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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
import JudulList from '../../_com/judulList'; Button,
Center,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Group,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -14,9 +32,10 @@ function InfoWabahPenyakit() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Info Wabah Penyakit' title='Info Wabah Penyakit'
placeholder='pencarian' placeholder='Cari judul atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -46,64 +65,99 @@ function ListInfoWabahPenyakit({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Info Wabah Penyakit' <Title order={4}>Daftar Info Wabah Penyakit</Title>
href='/admin/kesehatan/info-wabah-penyakit/create' <Tooltip label="Tambah Info Wabah" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() => router.push('/admin/kesehatan/info-wabah-penyakit/create')}
<TableTh>Judul</TableTh> >
<TableTh>Deskripsi Singkat</TableTh> Tambah Baru
<TableTh>Image</TableTh> </Button>
<TableTh>Detail</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead>
<TableTbody> {/* Tabel */}
{filteredData.map((item) => ( <Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={100}> <Text truncate fz="sm" c="dimmed">
<Text truncate="end" fz={"sm"}>{item.deskripsiSingkat}</Text> {item.deskripsiSingkat}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" /> <Image w={100} src={item.image?.link} alt="image" radius="md" />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/info-wabah-penyakit/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">
Tidak ada data info wabah penyakit yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<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

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat'; import kontakDarurat from '@/app/admin/(dashboard)/_state/kesehatan/kontak-darurat/kontakDarurat';
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,11 +24,10 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditKontakDarurat() { function EditKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat) const kontakDaruratState = useProxy(kontakDarurat);
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);
@@ -25,10 +35,10 @@ function EditKontakDarurat() {
name: kontakDaruratState.edit.form.name || '', name: kontakDaruratState.edit.form.name || '',
deskripsi: kontakDaruratState.edit.form.deskripsi || '', deskripsi: kontakDaruratState.edit.form.deskripsi || '',
imageId: kontakDaruratState.edit.form.imageId || '', imageId: kontakDaruratState.edit.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const loadProgramKesehatan = async () => { const loadKontakDarurat = async () => {
const id = params?.id as string; const id = params?.id as string;
if (!id) return; if (!id) return;
@@ -46,12 +56,12 @@ function EditKontakDarurat() {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program kesehatan:", error); console.error("Error loading kontak darurat:", error);
toast.error("Gagal memuat data program kesehatan"); toast.error("Gagal memuat data kontak darurat");
} }
}; };
loadProgramKesehatan(); loadKontakDarurat();
}, [params?.id]); }, [params?.id]);
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -79,95 +89,116 @@ function EditKontakDarurat() {
router.push("/admin/kesehatan/kontak-darurat"); router.push("/admin/kesehatan/kontak-darurat");
} catch (error) { } catch (error) {
console.error("Error updating kontak darurat:", error); console.error("Error updating kontak darurat:", error);
toast.error("Gagal memuat data kontak darurat"); toast.error("Terjadi kesalahan saat memperbarui kontak darurat");
} }
} };
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} />
<Stack gap={"xs"}> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Kontak Darurat</Title> Edit Kontak Darurat
<TextInput </Title>
value={formData.name} </Group>
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>} {/* Form */}
placeholder="masukkan judul" <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
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label="Judul"
placeholder="Masukkan judul"
required
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
<Box> </Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor <Box>
value={formData.deskripsi} <Text fz="sm" fw="bold">Gambar</Text>
onChange={(val) => setFormData({ ...formData, deskripsi: val })} <Dropzone
/> onDrop={(files) => {
</Box> const selectedFile = files[0];
<Box> if (selectedFile) {
<Text fz={"md"} fw={"bold"}>Gambar</Text> setFile(selectedFile);
<Box> setPreviewImage(URL.createObjectURL(selectedFile));
<Dropzone }
onDrop={(files) => { }}
const selectedFile = files[0]; // Ambil file pertama onReject={() => toast.error('File tidak valid.')}
if (selectedFile) { maxSize={5 * 1024 ** 2}
setFile(selectedFile); accept={{ 'image/*': [] }}
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview >
} <Group justify="center" gap="xl" mih={200} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="lg" fw={500}>Drag gambar ke sini atau klik untuk pilih file</Text>
<Text size="sm" c="dimmed" mt={5}>Maksimal 5MB dan format gambar</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
onReject={() => toast.error('File tidak valid.')} />
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> </Box>
</Box> )}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
<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 Simpan
</Button> </Button>
</Stack> </Group>
</Paper> </Stack>
</Stack> </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';
@@ -10,82 +10,129 @@ import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat'; import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
function DetailKontakDarurat() { function DetailKontakDarurat() {
const kontakDaruratState = useProxy(kontakDarurat) const state = useProxy(kontakDarurat);
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(() => {
kontakDaruratState.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
kontakDaruratState.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/kontak-darurat") router.push("/admin/kesehatan/kontak-darurat");
} }
} };
if (!kontakDaruratState.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={400} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = state.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Kontak Darurat</Text> Kembali
{kontakDaruratState.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fz={"lg"} fw={"bold"}>Judul</Text> withBorder
<Text fz={"lg"}>{kontakDaruratState.findUnique.data.name}</Text> w={{ base: "100%", md: "50%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> radius="md"
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: kontakDaruratState.findUnique.data.deskripsi }} /> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Image src={kontakDaruratState.findUnique.data.image?.link} alt="gambar" /> Detail Kontak Darurat
</Box> </Text>
<Box>
<Flex gap={"xs"}> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Button color="red" onClick={() => { <Stack gap="sm">
if (kontakDaruratState.findUnique.data) { <Box>
setSelectedId(kontakDaruratState.findUnique.data.id) <Text fz="lg" fw="bold">Judul</Text>
setModalHapus(true) <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="gambar"
radius="md"
maw={300}
/>
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}} }}
disabled={kontakDaruratState.delete.loading || !kontakDaruratState.findUnique.data} variant="light"
> radius="md"
<IconX size={20} /> size="md"
</Button> disabled={state.delete.loading}
<Button onClick={() => router.push(`/admin/kesehatan/kontak-darurat/${kontakDaruratState.findUnique.data?.id}/edit`)} color="green"> >
<IconEdit size={20} /> <IconTrash size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box>
</Stack> <Tooltip label="Edit Data" withArrow position="top">
</Paper> <Button
) : null} color="green"
onClick={() =>
router.push(`/admin/kesehatan/kontak-darurat/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -1,8 +1,24 @@
'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 {
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react'; Box,
Button,
Group,
Image,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
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';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -11,18 +27,17 @@ import CreateEditor from '../../../_com/createEditor';
import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat'; import kontakDarurat from '../../../_state/kesehatan/kontak-darurat/kontakDarurat';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
function CreateKontakDarurat() { function CreateKontakDarurat() {
const router = useRouter(); const router = useRouter();
const kontakDaruratState = useProxy(kontakDarurat) const kontakDaruratState = useProxy(kontakDarurat);
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 = () => {
kontakDaruratState.create.form = { kontakDaruratState.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
@@ -30,7 +45,7 @@ function CreateKontakDarurat() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -40,7 +55,7 @@ function CreateKontakDarurat() {
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');
} }
kontakDaruratState.create.form.imageId = uploaded.id; kontakDaruratState.create.form.imageId = uploaded.id;
@@ -48,27 +63,46 @@ function CreateKontakDarurat() {
await kontakDaruratState.create.create(); await kontakDaruratState.create.create();
resetForm(); resetForm();
router.push("/admin/kesehatan/kontak-darurat") router.push('/admin/kesehatan/kontak-darurat');
} };
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
</Box> variant="subtle"
onClick={() => router.back()}
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> p="xs"
<Stack gap="xs"> radius="md"
<Title order={3}>Create Kontak Darurat</Title> >
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Kontak Darurat
</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
value={kontakDaruratState.create.form.name} value={kontakDaruratState.create.form.name}
onChange={(val) => { onChange={(val) => {
kontakDaruratState.create.form.name = val.target.value; kontakDaruratState.create.form.name = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Judul</Text>} label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul" placeholder="Masukkan judul"
required
/> />
<Box> <Box>
@@ -80,64 +114,91 @@ function CreateKontakDarurat() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fz="sm" fw="bold">Gambar</Text>
<Box> <Dropzone
<Dropzone onDrop={(files) => {
onDrop={(files) => { const selectedFile = files[0];
const selectedFile = files[0]; // Ambil file pertama if (selectedFile) {
if (selectedFile) { setFile(selectedFile);
setFile(selectedFile); setPreviewImage(URL.createObjectURL(selectedFile));
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview }
} }}
}} onReject={() => toast.error('File tidak valid.')}
onReject={() => toast.error('File tidak valid.')} maxSize={5 * 1024 ** 2}
maxSize={5 * 1024 ** 2} // Maks 5MB accept={{ 'image/*': [] }}
accept={{ 'image/*': [] }} >
<Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: 'none' }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Dropzone.Accept>
<Dropzone.Accept> <IconUpload
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> size={52}
</Dropzone.Accept> color="var(--mantine-color-blue-6)"
<Dropzone.Reject> stroke={1.5}
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/> />
</Box> </Dropzone.Accept>
)} <Dropzone.Reject>
</Box> <IconX
size={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan <Group justify="right">
</Button> <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,26 +1,46 @@
'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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
import JudulList from '../../_com/judulList'; Button,
import HeaderSearch from '../../_com/header'; Center,
import { useRouter } from 'next/navigation'; Image,
import { useProxy } from 'valtio/utils'; Pagination,
import kontakDarurat from '../../_state/kesehatan/kontak-darurat/kontakDarurat'; 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 { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import kontakDarurat from '../../_state/kesehatan/kontak-darurat/kontakDarurat';
function KontakDarurat() { function KontakDarurat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='Kontak Darurat' title='Kontak Darurat'
placeholder='pencarian' placeholder='Cari judul atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListKontakDarurat search={search} /> <ListKontakDarurat search={search} />
</Box> </Box>
); );
@@ -30,13 +50,7 @@ function ListKontakDarurat({ search }: { search: string }) {
const kontakDaruratState = useProxy(kontakDarurat) const kontakDaruratState = useProxy(kontakDarurat)
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = kontakDaruratState.findMany;
data,
page,
totalPages,
loading,
load,
} = kontakDaruratState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10, search)
@@ -46,65 +60,97 @@ function ListKontakDarurat({ search }: { search: string }) {
if (loading || !data) { if (loading || !data) {
return ( return (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Stack mb="md" gap="sm">
title='List Kontak Darurat' <Box display="flex" style={{ justifyContent: "space-between", alignItems: "center" }}>
href='/admin/kesehatan/kontak-darurat/create' <Title order={4}>Daftar Kontak Darurat</Title>
/> <Tooltip label="Tambah Kontak Darurat" withArrow>
<Box style={{ overflowX: "auto" }}> <Button
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Judul</TableTh> onClick={() => router.push('/admin/kesehatan/kontak-darurat/create')}
<TableTh>Deskripsi</TableTh> >
<TableTh>Image</TableTh> Tambah Baru
<TableTh>Detail</TableTh> </Button>
</TableTr> </Tooltip>
</TableThead> </Box>
<TableTbody> </Stack>
{filteredData.map((item) => (
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={100}> <Text truncate fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" /> <Image w={100} src={item.image?.link} alt="image" radius="md" />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/kontak-darurat/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/kontak-darurat/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada data kontak darurat yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<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

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat'; import penangananDarurat from '@/app/admin/(dashboard)/_state/kesehatan/penanganan-darurat/penangananDarurat';
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';
@@ -77,97 +88,120 @@ function EditPenangananDarurat() {
router.push("/admin/kesehatan/penanganan-darurat"); router.push("/admin/kesehatan/penanganan-darurat");
} catch (error) { } catch (error) {
console.error("Error updating penanganan darurat:", error); console.error("Error updating penanganan darurat:", error);
toast.error("Gagal memuat data penanganan darurat"); toast.error("Gagal memperbarui data penanganan darurat");
} }
} }
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} />
<Stack gap={"xs"}> </Button>
<Paper bg={colors['white-1']} p={"md"} w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Penanganan Darurat</Title> Edit Penanganan Darurat
</Title>
</Group>
<TextInput {/* Form */}
value={formData.name} <Paper
onChange={(e) => setFormData({ ...formData, name: e.target.value })} w={{ base: '100%', md: '50%' }}
label={<Text fz="sm" fw="bold">Judul</Text>} bg={colors['white-1']}
placeholder="masukkan judul" p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label="Judul"
placeholder="Masukkan judul"
required
/>
<Box>
<Text fz="sm" fw="bold">Deskripsi</Text>
<EditEditor
value={formData.deskripsi}
onChange={(val) => setFormData({ ...formData, deskripsi: val })}
/> />
</Box>
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Text fz="sm" fw="bold">Gambar</Text>
<EditEditor <Dropzone
value={formData.deskripsi} onDrop={(files) => {
onChange={(val) => setFormData({ ...formData, deskripsi: val })} const selectedFile = files[0];
/> if (selectedFile) {
</Box> setFile(selectedFile);
<Box> setPreviewImage(URL.createObjectURL(selectedFile));
<Text fz={"md"} fw={"bold"}>Gambar</Text> }
<Box> }}
<Dropzone onReject={() => toast.error('File tidak valid.')}
onDrop={(files) => { maxSize={5 * 1024 ** 2}
const selectedFile = files[0]; // Ambil file pertama accept={{ 'image/*': [] }}
if (selectedFile) { >
setFile(selectedFile); <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview <Dropzone.Accept>
} <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
onReject={() => toast.error('File tidak valid.')} />
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> </Box>
</Box> )}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
<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 Simpan
</Button> </Button>
</Stack> </Group>
</Paper> </Stack>
</Stack > </Paper>
</Box > </Box>
); );
} }

View File

@@ -1,93 +1,134 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter, useParams } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { Skeleton } from '@mantine/core';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function DetailPenangananDarurat() { function DetailPenangananDarurat() {
const penangananDaruratState = useProxy(penangananDarurat) const state = useProxy(penangananDarurat);
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(() => {
penangananDaruratState.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
penangananDaruratState.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/penanganan-darurat") router.push("/admin/kesehatan/penanganan-darurat");
} }
};
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
} }
if (!penangananDaruratState.findUnique.data) { const data = state.findUnique.data;
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Back */}
<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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Penanganan Darurat</Text> Kembali
{penangananDaruratState.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> {/* Wrapper Detail */}
<Box> <Paper
<Text fz={"lg"} fw={"bold"}>Nama Penanganan Darurat</Text> withBorder
<Text fz={"lg"}>{penangananDaruratState.findUnique.data.name}</Text> w={{ base: "100%", md: "50%" }}
</Box> bg={colors['white-1']}
<Box> p="lg"
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> radius="md"
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: penangananDaruratState.findUnique.data.deskripsi }} /> shadow="sm"
</Box> >
<Box> <Stack gap="md">
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Image src={penangananDaruratState.findUnique.data.image?.link} alt="gambar" /> Detail Penanganan Darurat
</Box> </Text>
<Box>
<Flex gap={"xs"}> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Button color="red" onClick={() => { <Stack gap="sm">
if (penangananDaruratState.findUnique.data) { <Box>
setSelectedId(penangananDaruratState.findUnique.data.id) <Text fz="lg" fw="bold">Nama Penanganan Darurat</Text>
setModalHapus(true) <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>
<Image
src={data.image?.link}
alt="gambar penanganan darurat"
radius="md"
mah={250}
fit="contain"
/>
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}} }}
disabled={penangananDaruratState.delete.loading || !penangananDaruratState.findUnique.data}> variant="light"
<IconX size={20} /> radius="md"
</Button> size="md"
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${penangananDaruratState.findUnique.data?.id}/edit`)} color="green"> >
<IconEdit size={20} /> <IconTrash size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box>
</Stack> <Tooltip label="Edit Data" withArrow position="top">
</Paper> <Button
) : null} color="green"
onClick={() =>
router.push(`/admin/kesehatan/penanganan-darurat/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -1,9 +1,25 @@
'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 { 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';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -11,18 +27,17 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat'; import penangananDarurat from '../../../_state/kesehatan/penanganan-darurat/penangananDarurat';
function CreatePenangananDarurat() { function CreatePenangananDarurat() {
const router = useRouter(); const router = useRouter();
const penangananDaruratState = useProxy(penangananDarurat) const penangananDaruratState = useProxy(penangananDarurat);
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 = () => {
penangananDaruratState.create.form = { penangananDaruratState.create.form = {
name: "", name: '',
deskripsi: "", deskripsi: '',
imageId: "", imageId: '',
}; };
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
@@ -30,7 +45,7 @@ function CreatePenangananDarurat() {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Pilih file gambar terlebih dahulu');
} }
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
@@ -40,7 +55,7 @@ function CreatePenangananDarurat() {
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');
} }
penangananDaruratState.create.form.imageId = uploaded.id; penangananDaruratState.create.form.imageId = uploaded.id;
@@ -48,31 +63,52 @@ function CreatePenangananDarurat() {
await penangananDaruratState.create.create(); await penangananDaruratState.create.create();
resetForm(); resetForm();
router.push("/admin/kesehatan/penanganan-darurat") router.push('/admin/kesehatan/penanganan-darurat');
} };
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
</Box> variant="subtle"
onClick={() => router.back()}
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> p="xs"
<Stack gap="xs"> radius="md"
<Title order={3}>Create Penanganan Darurat</Title> >
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Penanganan Darurat
</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">
{/* Judul */}
<TextInput <TextInput
label={<Text fw="bold" fz="sm">Judul</Text>}
placeholder="Masukkan judul"
value={penangananDaruratState.create.form.name} value={penangananDaruratState.create.form.name}
onChange={(val) => { onChange={(val) => {
penangananDaruratState.create.form.name = val.target.value; penangananDaruratState.create.form.name = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Judul</Text>} required
placeholder="masukkan judul"
/> />
{/* Deskripsi */}
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Text fw="bold" fz="sm">Deskripsi</Text>
<CreateEditor <CreateEditor
value={penangananDaruratState.create.form.deskripsi} value={penangananDaruratState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -80,30 +116,49 @@ function CreatePenangananDarurat() {
}} }}
/> />
</Box> </Box>
{/* Upload Gambar */}
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Text fw="bold" fz="sm">Gambar</Text>
<Box> <Box>
<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.')}
maxSize={5 * 1024 ** 2} // Maks 5MB maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }} accept={{ 'image/*': [] }}
> >
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}> <Group
justify="center"
gap="xl"
mih={220}
style={{ pointerEvents: 'none' }}
>
<Dropzone.Accept> <Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <IconUpload
size={52}
color="var(--mantine-color-blue-6)"
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={52}
color="var(--mantine-color-red-6)"
stroke={1.5}
/>
</Dropzone.Reject> </Dropzone.Reject>
<Dropzone.Idle> <Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <IconPhoto
size={52}
color="var(--mantine-color-dimmed)"
stroke={1.5}
/>
</Dropzone.Idle> </Dropzone.Idle>
<div> <div>
@@ -117,7 +172,6 @@ function CreatePenangananDarurat() {
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && ( {previewImage && (
<Box mt="sm"> <Box mt="sm">
<Image <Image
@@ -133,12 +187,24 @@ function CreatePenangananDarurat() {
/> />
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan {/* Button 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,8 +1,26 @@
'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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
import JudulList from '../../_com/judulList'; Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
@@ -14,100 +32,135 @@ function PenangananDarurat() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header Search */}
<HeaderSearch <HeaderSearch
title='PenangananDarurat' title='Penanganan Darurat'
placeholder='pencarian' placeholder='Cari judul atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListPenangananDarurat search={search} /> <ListPenangananDarurat search={search} />
</Box> </Box>
); );
} }
function ListPenangananDarurat({ search }: { search: string }) { function ListPenangananDarurat({ search }: { search: string }) {
const penangananDaruratState = useProxy(penangananDarurat) const state = useProxy(penangananDarurat);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = state.findMany;
data,
page,
totalPages,
loading,
load,
} = penangananDaruratState.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 (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> {/* Judul + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Penanganan Darurat' <Title order={4}>Daftar Penanganan Darurat</Title>
href='/admin/kesehatan/penanganan-darurat/create' <Tooltip label="Tambah Penanganan Darurat" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() => router.push('/admin/kesehatan/penanganan-darurat/create')}
<TableTh>Judul</TableTh> >
<TableTh>Deskripsi</TableTh> Tambah Baru
<TableTh>Image</TableTh> </Button>
<TableTh>Detail</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead>
<TableTbody> {/* Tabel */}
{filteredData.map((item) => ( <Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Gambar</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box></TableTd> </Text>
</TableTd>
<TableTd> <TableTd>
<Box w={100}> <Text
<Text lineClamp={1} truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> fz="sm"
</Box> c="dimmed"
truncate
lineClamp={1}
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" /> <Image w={100} src={item.image?.link} alt="image" />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/penanganan-darurat/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() =>
router.push(`/admin/kesehatan/penanganan-darurat/${item.id}`)
}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada data penanganan darurat</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<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>
) );
} }
export default PenangananDarurat; export default PenangananDarurat;

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu'; import posyandustate from '@/app/admin/(dashboard)/_state/kesehatan/posyandu/posyandu';
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';
@@ -14,185 +25,231 @@ import { useProxy } from 'valtio/utils';
function EditPosyandu() { function EditPosyandu() {
const statePosyandu = useProxy(posyandustate) const statePosyandu = useProxy(posyandustate);
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 [formData, setFormData] = useState({
name: statePosyandu.edit.form.name || '',
nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '',
jadwalPelayanan: statePosyandu.edit.form.jadwalPelayanan || '',
});
useEffect(() => { const [previewImage, setPreviewImage] = useState<string | null>(null);
const loadPosyandu = async () => { const [file, setFile] = useState<File | null>(null);
const id = params?.id as string; const [formData, setFormData] = useState({
if (!id) return; name: statePosyandu.edit.form.name || '',
nomor: statePosyandu.edit.form.nomor || '',
deskripsi: statePosyandu.edit.form.deskripsi || '',
imageId: statePosyandu.edit.form.imageId || '',
jadwalPelayanan: statePosyandu.edit.form.jadwalPelayanan || '',
});
try {
const data = await statePosyandu.edit.load(id);
if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
jadwalPelayanan: data.jadwalPelayanan || '',
});
if (data?.image?.link) { useEffect(() => {
setPreviewImage(data.image.link); const loadPosyandu = async () => {
} const id = params?.id as string;
} if (!id) return;
} catch (error) {
console.error("Error loading posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
loadPosyandu();
}, [params?.id])
const handleSubmit = async () => {
try {
statePosyandu.edit.form = {
...statePosyandu.edit.form,
name: formData.name,
nomor: formData.nomor,
deskripsi: formData.deskripsi,
imageId: formData.imageId,
jadwalPelayanan: formData.jadwalPelayanan,
}
if (file) { try {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name }); const data = await statePosyandu.edit.load(id);
const uploaded = res.data?.data; if (data) {
setFormData({
name: data.name || '',
nomor: data.nomor || '',
deskripsi: data.deskripsi || '',
imageId: data.imageId || '',
jadwalPelayanan: data.jadwalPelayanan || '',
});
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.edit.form.imageId = uploaded.id; if (data?.image?.link) {
} setPreviewImage(data.image.link);
}
}
} catch (error) {
console.error("Error loading posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
};
loadPosyandu();
}, [params?.id]);
await statePosyandu.edit.update();
toast.success("Posyandu berhasil diperbarui!");
router.push("/admin/kesehatan/posyandu");
} catch (error) {
console.error("Error updating posyandu:", error);
toast.error("Gagal memuat data posyandu");
}
}
return ( const handleSubmit = async () => {
<Box> try {
<Box mb={10}> statePosyandu.edit.form = {
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> ...statePosyandu.edit.form,
<IconArrowBack color={colors['blue-button']} size={25} /> ...formData,
</Button> };
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Posyandu</Title>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> if (file) {
<Text size="xl" inline> const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
Drag gambar ke sini atau klik untuk pilih file const uploaded = res.data?.data;
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> if (!uploaded?.id) {
</Box> return toast.error("Gagal upload gambar");
<TextInput }
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>} statePosyandu.edit.form.imageId = uploaded.id;
placeholder='Masukkan nama posyandu' }
/>
<TextInput
value={formData.nomor} await statePosyandu.edit.update();
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })} toast.success("Posyandu berhasil diperbarui!");
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>} router.push("/admin/kesehatan/posyandu");
placeholder='Masukkan nomor posyandu' } catch (error) {
/> console.error("Error updating posyandu:", error);
<Box> toast.error("Terjadi kesalahan saat memperbarui posyandu");
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text> }
<EditEditor };
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent }); return (
statePosyandu.edit.form.deskripsi = htmlContent; <Box px={{ base: 'sm', md: 'lg' }} py="md">
}} {/* Tombol Back */}
/> <Group mb="md">
</Box> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Box> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<Text fw={"bold"} fz={"sm"}>Jadwal Pelayanan</Text> <IconArrowBack color={colors['blue-button']} size={24} />
<EditEditor </Button>
value={formData.jadwalPelayanan} </Tooltip>
onChange={(htmlContent) => { <Title order={4} ml="sm" c="dark">
setFormData({ ...formData, jadwalPelayanan: htmlContent }); Edit Posyandu
statePosyandu.edit.form.jadwalPelayanan = htmlContent; </Title>
}} </Group>
/>
</Box>
<Group> {/* Card utama */}
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> <Paper
</Group> w={{ base: '100%', md: '50%' }}
</Stack> bg={colors['white-1']}
</Paper> p="lg"
</Box> radius="md"
); shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Posyandu
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="red" stroke={1.5} />
</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>
{previewImage && (
<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']}`,
}}
/>
</Box>
)}
</Box>
{/* Input Form */}
<TextInput
label="Nama Posyandu"
placeholder="Masukkan nama posyandu"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nomor Posyandu"
placeholder="Masukkan nomor posyandu"
value={formData.nomor}
onChange={(e) => setFormData({ ...formData, nomor: e.target.value })}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>Deskripsi Posyandu</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) => {
setFormData({ ...formData, deskripsi: htmlContent });
statePosyandu.edit.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>Jadwal Pelayanan</Text>
<EditEditor
value={formData.jadwalPelayanan}
onChange={(htmlContent) => {
setFormData({ ...formData, jadwalPelayanan: htmlContent });
statePosyandu.edit.form.jadwalPelayanan = htmlContent;
}}
/>
</Box>
{/* 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>
</Paper>
</Box>
);
} }
export default EditPosyandu;
export default EditPosyandu;

View File

@@ -1,105 +1,175 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core'; import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } from '@tabler/icons-react'; import { IconArrowBack, IconTrash, IconEdit } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu'; import posyanduState from '../../../_state/kesehatan/posyandu/posyandu';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPosyandu() { function DetailPosyandu() {
const statePosyandu = useProxy(posyandustate) const statePosyandu = useProxy(posyanduState);
const params = useParams() const params = useParams();
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);
useShallowEffect(() => {
statePosyandu.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => { useShallowEffect(() => {
if (selectedId) { statePosyandu.findUnique.load(params?.id as string);
statePosyandu.delete.byId(selectedId) }, []);
setModalHapus(false)
setSelectedId(null)
router.push("/admin/kesehatan/posyandu")
}
}
if (!statePosyandu.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return ( const handleHapus = () => {
<Box> if (selectedId) {
<Box mb={10}> statePosyandu.delete.byId(selectedId);
<Button variant="subtle" onClick={() => router.back()}> setModalHapus(false);
<IconArrowBack color={colors['blue-button']} size={25} /> setSelectedId(null);
</Button> router.push("/admin/kesehatan/posyandu");
</Box> }
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> };
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Posyandu</Text>
{statePosyandu.findUnique.data ? (
<Paper key={statePosyandu.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.name}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Nomor Posyandu</Text>
<Text fz={"lg"}>{statePosyandu.findUnique.data.nomor}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Posyandu</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.deskripsi }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Jadwal Pelayanan</Text>
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: statePosyandu.findUnique.data.jadwalPelayanan }} />
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text>
<Image src={statePosyandu.findUnique.data.image?.link} alt="gambar" />
</Box>
<Box>
<Flex gap={"xs"}>
<Button onClick={() => {
if (statePosyandu.findUnique.data) {
setSelectedId(statePosyandu.findUnique.data.id)
setModalHapus(true)
}
}} color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${statePosyandu.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
<ModalKonfirmasiHapus
opened={modalHapus} if (!statePosyandu.findUnique.data) {
onClose={() => setModalHapus(false)} return (
onConfirm={handleHapus} <Stack py={10}>
text="Apakah anda yakin ingin menghapus posyandu ini?" <Skeleton height={500} radius="md" />
/> </Stack>
</Box> );
); }
const data = statePosyandu.findUnique.data;
return (
<Box py={10}>
{/* Tombol kembali */}
<Button
variant="subtle"
onClick={() => router.back()}
leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
mb={15}
>
Kembali
</Button>
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Posyandu
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box>
<Text fz="lg" fw="bold">Nama Posyandu</Text>
<Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Posyandu</Text>
<Text fz="md" c="dimmed">{data.nomor || '-'}</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">Jadwal Pelayanan</Text>
<Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.jadwalPelayanan || '-' }}
/>
</Box>
<Box>
<Text fz="lg" fw="bold">Gambar</Text>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || 'Gambar Posyandu'}
w={200}
h={200}
radius="md"
fit="cover"
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
)}
</Box>
{/* Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Posyandu" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Posyandu" withArrow position="top">
<Button
color="green"
onClick={() => router.push(`/admin/kesehatan/posyandu/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal konfirmasi hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah Anda yakin ingin menghapus posyandu ini?"
/>
</Box>
);
} }
export default DetailPosyandu;
export default DetailPosyandu;

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 { 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';
@@ -11,159 +22,193 @@ import { useProxy } from 'valtio/utils';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import posyandustate from '../../../_state/kesehatan/posyandu/posyandu'; import posyandustate from '../../../_state/kesehatan/posyandu/posyandu';
function CreatePosyandu() { function CreatePosyandu() {
const statePosyandu = useProxy(posyandustate) const statePosyandu = useProxy(posyandustate);
const router = useRouter(); const router = useRouter();
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => {
statePosyandu.create.form = {
name: "",
nomor: "",
deskripsi: "",
imageId: "",
jadwalPelayanan: "",
};
setFile(null);
setPreviewImage(null);
}
const handleSubmit = async () => {
if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
statePosyandu.create.form.imageId = uploaded.id;
await statePosyandu.create.create();
resetForm();
router.push("/admin/kesehatan/posyandu")
}
const resetForm = () => {
statePosyandu.create.form = {
name: '',
nomor: '',
deskripsi: '',
imageId: '',
jadwalPelayanan: '',
};
setFile(null);
setPreviewImage(null);
};
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'}> const handleSubmit = async () => {
<Stack gap={"xs"}> if (!file) {
<Title order={4}>Create Posyandu</Title> return toast.warn('Silakan pilih file gambar terlebih dahulu');
<Box> }
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */} // Upload gambar dulu
{previewImage && ( const res = await ApiFetch.api.fileStorage.create.post({
<Box mt="sm"> file,
<Image name: file.name,
src={previewImage} });
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
</Box> const uploaded = res.data?.data;
<TextInput if (!uploaded?.id) {
label={<Text fw={"bold"} fz={"sm"}>Nama Posyandu</Text>} return toast.error('Gagal upload gambar');
placeholder='Masukkan nama posyandu' }
value={statePosyandu.create.form.name}
onChange={(e) => {
statePosyandu.create.form.name = e.target.value; statePosyandu.create.form.imageId = uploaded.id;
}}
/>
<TextInput await statePosyandu.create.create();
label={<Text fw={"bold"} fz={"sm"}>Nomor Posyandu</Text>}
placeholder='Masukkan nomor posyandu'
value={statePosyandu.create.form.nomor} resetForm();
onChange={(e) => { router.push('/admin/kesehatan/posyandu');
statePosyandu.create.form.nomor = e.target.value; };
}}
/>
<Box> return (
<Text fw={"bold"} fz={"sm"}>Deskripsi Posyandu</Text> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<CreateEditor {/* Header */}
value={statePosyandu.create.form.deskripsi} <Group mb="md">
onChange={(htmlContent) => { <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
statePosyandu.create.form.deskripsi = htmlContent; <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
}} <IconArrowBack color={colors['blue-button']} size={24} />
/> </Button>
</Box> </Tooltip>
<Box> <Title order={4} ml="sm" c="dark">
<Text fw={"bold"} fz={"sm"}>Jadwal Pelayanan</Text> Tambah Posyandu
<CreateEditor </Title>
value={statePosyandu.create.form.jadwalPelayanan} </Group>
onChange={(htmlContent) => {
statePosyandu.create.form.jadwalPelayanan = htmlContent;
}} <Paper
/> w={{ base: '100%', md: '50%' }}
</Box> bg={colors['white-1']}
<Group> p="lg"
<Button onClick={handleSubmit} bg={colors['blue-button']}>Submit</Button> radius="md"
</Group> shadow="sm"
</Stack> style={{ border: '1px solid #e0e0e0' }}
</Paper> >
</Box> <Stack gap="md">
); {/* Upload Gambar */}
<Box>
<Text fw="bold" fz="sm" mb={6}>
Gambar Posyandu
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={48} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={48} color="var(--mantine-color-red-6)" stroke={1.5} />
</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>
{previewImage && (
<Box mt="sm" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
/>
</Box>
)}
</Box>
{/* Input Form */}
<TextInput
label="Nama Posyandu"
placeholder="Masukkan nama posyandu"
value={statePosyandu.create.form.name || ''}
onChange={(e) => (statePosyandu.create.form.name = e.target.value)}
required
/>
<TextInput
label="Telepon Posyandu"
placeholder="Masukkan telepon posyandu"
value={statePosyandu.create.form.nomor || ''}
onChange={(e) => (statePosyandu.create.form.nomor = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Posyandu
</Text>
<CreateEditor
value={statePosyandu.create.form.deskripsi}
onChange={(htmlContent) => {
statePosyandu.create.form.deskripsi = htmlContent;
}}
/>
</Box>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Jadwal Pelayanan
</Text>
<CreateEditor
value={statePosyandu.create.form.jadwalPelayanan}
onChange={(htmlContent) => {
statePosyandu.create.form.jadwalPelayanan = htmlContent;
}}
/>
</Box>
{/* 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>
</Paper>
</Box>
);
} }
export default CreatePosyandu;
export default CreatePosyandu;

View File

@@ -1,114 +1,170 @@
'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, Text } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
import HeaderSearch from '../../_com/header'; Button,
import JudulList from '../../_com/judulList'; Center,
import { useRouter } from 'next/navigation'; Group,
import { useProxy } from 'valtio/utils'; Pagination,
import posyandustate from '../../_state/kesehatan/posyandu/posyandu'; 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, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import posyandustate from '../../_state/kesehatan/posyandu/posyandu';
function Posyandu() { function Posyandu() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Posyandu' title='Posyandu'
placeholder='pencarian' placeholder='Cari nama posyandu atau nomor...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListPosyandu search={search} /> <ListPosyandu search={search} />
</Box> </Box>
); );
} }
function ListPosyandu({ search }: { search: string }) { function ListPosyandu({ search }: { search: string }) {
const statePosyandu = useProxy(posyandustate) const statePosyandu = useProxy(posyandustate)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = statePosyandu.findMany;
useShallowEffect(() => { const {
load(page, 10, search) data,
}, [page, search]) page,
totalPages,
loading,
load,
} = statePosyandu.findMany;
const filteredData = data || [];
if (loading || !data) { useShallowEffect(() => {
return ( load(page, 10, search)
<Box py={10}> }, [page, search])
<Skeleton h={500} />
</Box>
)
}
return (
<Box py={10}> const filteredData = data || [];
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Posyandu' if (loading || !data) {
href='/admin/kesehatan/posyandu/create' return (
/> <Stack py={10}>
<Box style={{ overflowX: "auto" }}> <Skeleton height={600} radius="md" />
<Table striped withTableBorder withRowBorders> </Stack>
<TableThead> )
<TableTr> }
<TableTh>Nama Posyandu</TableTh>
<TableTh>Nomor Posyandu</TableTh>
<TableTh>Deskripsi</TableTh> return (
<TableTh>Detail</TableTh> <Box py={10}>
</TableTr> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
</TableThead> <Group justify="space-between" mb="md">
<TableTbody> <Title order={4}>Daftar Posyandu</Title>
{filteredData.map((item) => ( <Tooltip label="Tambah Posyandu" withArrow>
<TableTr key={item.id}> <Button
<TableTd> leftSection={<IconPlus size={18} />}
<Box w={100}> color="blue"
<Text truncate="end" lineClamp={1} fz={"sm"}>{item.name}</Text> variant="light"
</Box> onClick={() => router.push('/admin/kesehatan/posyandu/create')}
</TableTd> >
<TableTd> Tambah Baru
<Box w={100}> </Button>
<Text truncate="end" lineClamp={1} fz={"sm"}>{item.nomor}</Text> </Tooltip>
</Box> </Group>
</TableTd> <Box style={{ overflowX: "auto" }}>
<TableTd> <Table highlightOnHover>
<Box w={100}> <TableThead>
<Text truncate="end" lineClamp={1} fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsi }} /> <TableTr>
</Box> <TableTh style={{ width: '25%' }}>Nama Posyandu</TableTh>
</TableTd> <TableTh style={{ width: '20%' }}>Nomor Posyandu</TableTh>
<TableTd> <TableTh style={{ width: '30%' }}>Deskripsi</TableTh>
<Button onClick={() => router.push(`/admin/kesehatan/posyandu/${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 style={{ width: '25%' }}>
</Box> <Text fw={500} truncate="end" lineClamp={1}>
</Paper> {item.name}
<Center> </Text>
<Pagination </TableTd>
value={page} <TableTd style={{ width: '20%' }}>
onChange={(newPage) => load(newPage)} // ini penting! <Text truncate fz="sm" c="dimmed">
total={totalPages} {item.nomor || '-'}
mt="md" </Text>
mb="md" </TableTd>
/> <TableTd style={{ width: '30%' }}>
</Center> <Text
</Box> truncate
); fz="sm"
dangerouslySetInnerHTML={{ __html: item.deskripsi }}
/>
</TableTd>
<TableTd style={{ width: '15%' }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/posyandu/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data posyandu yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</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>
);
} }
export default Posyandu;
export default Posyandu;

View File

@@ -4,7 +4,18 @@ import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
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';
@@ -12,11 +23,10 @@ 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 EditProgramKesehatan() { function EditProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan) const programKesehatanState = useProxy(programKesehatan);
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);
@@ -25,7 +35,7 @@ function EditProgramKesehatan() {
deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '', deskripsiSingkat: programKesehatanState.edit.form.deskripsiSingkat || '',
deskripsi: programKesehatanState.edit.form.deskripsi || '', deskripsi: programKesehatanState.edit.form.deskripsi || '',
imageId: programKesehatanState.edit.form.imageId || '', imageId: programKesehatanState.edit.form.imageId || '',
}) });
useEffect(() => { useEffect(() => {
const loadProgramKesehatan = async () => { const loadProgramKesehatan = async () => {
@@ -47,8 +57,8 @@ function EditProgramKesehatan() {
} }
} }
} catch (error) { } catch (error) {
console.error("Error loading program kesehatan:", error); console.error('Error loading program kesehatan:', error);
toast.error("Gagal memuat data program kesehatan"); toast.error('Gagal memuat data program kesehatan');
} }
}; };
@@ -70,114 +80,143 @@ function EditProgramKesehatan() {
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');
} }
programKesehatanState.edit.form.imageId = uploaded.id; programKesehatanState.edit.form.imageId = uploaded.id;
} }
await programKesehatanState.edit.update(); await programKesehatanState.edit.update();
toast.success("Program kesehatan berhasil diperbarui!"); toast.success('Program kesehatan berhasil diperbarui!');
router.push("/admin/kesehatan/program-kesehatan"); router.push('/admin/kesehatan/program-kesehatan');
} catch (error) { } catch (error) {
console.error("Error updating program kesehatan:", error); console.error('Error updating program kesehatan:', error);
toast.error("Gagal memuat data program kesehatan"); toast.error('Terjadi kesalahan saat memperbarui program kesehatan');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol back */}
<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} />
<Stack gap={"xs"}> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Program Kesehatan</Title> Edit Program Kesehatan
<TextInput </Title>
value={formData.name} </Group>
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label={<Text fz="sm" fw="bold">Judul</Text>}
placeholder="masukkan judul"
/>
<TextInput {/* Card Form */}
value={formData.deskripsiSingkat} <Paper
onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })} w={{ base: '100%', md: '50%' }}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>} bg={colors['white-1']}
placeholder="masukkan deskripsi" p="lg"
/> radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
label="Judul"
placeholder="Masukkan judul"
required
/>
<Box> <TextInput
<Text fz="sm" fw="bold">Deskripsi</Text> value={formData.deskripsiSingkat}
<EditEditor onChange={(e) => setFormData({ ...formData, deskripsiSingkat: e.target.value })}
value={formData.deskripsi} label="Deskripsi Singkat"
onChange={(val) => setFormData({ ...formData, deskripsi: val })} placeholder="Masukkan deskripsi singkat"
/> required
</Box> />
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Box>
<Box> <Text fz="sm" fw="bold" mb={6}>
<Dropzone Deskripsi
onDrop={(files) => { </Text>
const selectedFile = files[0]; // Ambil file pertama <EditEditor
if (selectedFile) { value={formData.deskripsi}
setFile(selectedFile); onChange={(val) => setFormData({ ...formData, deskripsi: val })}
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview />
} </Box>
<Box>
<Text fz="sm" fw="bold" mb={6}>
Gambar
</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
onReject={() => toast.error('File tidak valid.')} />
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> </Box>
</Box> )}
<Button onClick={handleSubmit} bg={colors['blue-button']}> </Box>
<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 Simpan
</Button> </Button>
</Stack> </Group>
</Paper> </Stack>
</Stack> </Paper>
</Box > </Box>
); );
} }

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core'; import { Box, Button, Group, Paper, Skeleton, Stack, Text, Tooltip, Image } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } 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 React, { useState } from 'react'; import React, { useState } from 'react';
import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '../../../_state/kesehatan/program-kesehatan/programKesehatan';
@@ -10,82 +10,116 @@ import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailProgramKesehatan() { function DetailProgramKesehatan() {
const programKesehatanState = useProxy(programKesehatan) const state = useProxy(programKesehatan);
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(() => {
programKesehatanState.findUnique.load(params?.id as string) state.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
programKesehatanState.delete.byId(selectedId) state.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/program-kesehatan") router.push("/admin/kesehatan/program-kesehatan");
} }
} };
if (!programKesehatanState.findUnique.data) { if (!state.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={400} /> <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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Program Kesehatan</Text> Kembali
{programKesehatanState.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
<Box> withBorder
<Text fz={"lg"} fw={"bold"}>Judul</Text> w={{ base: "100%", md: "50%" }}
<Text fz={"lg"}>{programKesehatanState.findUnique.data.name}</Text> bg={colors['white-1']}
</Box> p="lg"
<Box> radius="md"
<Text fz={"lg"} fw={"bold"}>Deskripsi Singkat</Text> shadow="sm"
<Text fz={"lg"}>{programKesehatanState.findUnique.data.deskripsiSingkat}</Text> >
</Box> <Stack gap="md">
<Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} fw={"bold"}>Deskripsi</Text> Detail Program Kesehatan
<Text fz={"lg"} dangerouslySetInnerHTML={{ __html: programKesehatanState.findUnique.data.deskripsi }} /> </Text>
</Box>
<Box> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Stack gap="sm">
<Image src={programKesehatanState.findUnique.data.image?.link} alt="gambar" /> <Box>
</Box> <Text fz="lg" fw="bold">Judul</Text>
<Box> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
<Flex gap={"xs"}> </Box>
<Button color="red" onClick={() => {
if (programKesehatanState.findUnique.data) { <Box>
setSelectedId(programKesehatanState.findUnique.data.id) <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
setModalHapus(true) <Text fz="md" c="dimmed">{data?.deskripsiSingkat || '-'}</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="gambar program kesehatan" radius="md" />
) : (
<Text fz="md" c="dimmed">-</Text>
)}
</Box>
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}} }}
disabled={programKesehatanState.delete.loading || !programKesehatanState.findUnique.data} variant="light"
> radius="md"
<IconX size={20} /> size="md"
</Button> >
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${programKesehatanState.findUnique.data?.id}/edit`)} color="green"> <IconTrash size={20} />
<IconEdit size={20} /> </Button>
</Button> </Tooltip>
</Flex>
</Box> <Tooltip label="Edit Data" withArrow position="top">
</Stack> <Button
</Paper> color="green"
) : null} onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${data?.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -94,7 +128,7 @@ function DetailProgramKesehatan() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kesehatan ini?" text="Apakah Anda yakin ingin menghapus program kesehatan ini?"
/> />
</Box> </Box>
); );

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';
@@ -13,30 +24,32 @@ import { Dropzone } from '@mantine/dropzone';
function CreateProgramKesehatan() { function CreateProgramKesehatan() {
const router = useRouter(); const router = useRouter();
const programKesehatanState = useProxy(programKesehatan) const programKesehatanState = useProxy(programKesehatan);
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 = () => {
// Reset state di valtio
programKesehatanState.create.form = { programKesehatanState.create.form = {
name: "", name: "",
deskripsiSingkat: "", deskripsiSingkat: "",
deskripsi: "", deskripsi: "",
imageId: "", imageId: "",
}; };
// Reset state lokal
setPreviewImage(null); setPreviewImage(null);
setFile(null); setFile(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!programKesehatanState.create.form.name) {
return toast.warn("Judul wajib diisi");
}
if (!programKesehatanState.create.form.deskripsiSingkat) {
return toast.warn("Deskripsi singkat wajib diisi");
}
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn("Pilih file gambar terlebih dahulu");
} }
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -47,34 +60,45 @@ function CreateProgramKesehatan() {
return toast.error("Gagal upload gambar"); return toast.error("Gagal upload gambar");
} }
// Simpan ID gambar ke form
programKesehatanState.create.form.imageId = uploaded.id; programKesehatanState.create.form.imageId = uploaded.id;
// Submit data berita
await programKesehatanState.create.create(); await programKesehatanState.create.create();
// Reset form setelah submit
resetForm(); resetForm();
router.push("/admin/kesehatan/program-kesehatan") router.push("/admin/kesehatan/program-kesehatan");
}; };
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 Program Kesehatan</Title> <Title order={4} ml="sm" c="dark">
Tambah Program Kesehatan
</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">
<TextInput <TextInput
value={programKesehatanState.create.form.name} value={programKesehatanState.create.form.name}
onChange={(val) => { onChange={(val) => {
programKesehatanState.create.form.name = val.target.value; programKesehatanState.create.form.name = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Judul</Text>} label="Judul"
placeholder="masukkan judul" placeholder="Masukkan judul"
required
/> />
<TextInput <TextInput
@@ -82,12 +106,15 @@ function CreateProgramKesehatan() {
onChange={(val) => { onChange={(val) => {
programKesehatanState.create.form.deskripsiSingkat = val.target.value; programKesehatanState.create.form.deskripsiSingkat = val.target.value;
}} }}
label={<Text fz="sm" fw="bold">Deskripsi Singkat</Text>} label="Deskripsi Singkat"
placeholder="masukkan deskripsi" placeholder="Masukkan deskripsi singkat"
required
/> />
<Box> <Box>
<Text fz="sm" fw="bold">Deskripsi</Text> <Title order={6} mb={6}>
Deskripsi
</Title>
<CreateEditor <CreateEditor
value={programKesehatanState.create.form.deskripsi} value={programKesehatanState.create.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -95,64 +122,76 @@ function CreateProgramKesehatan() {
}} }}
/> />
</Box> </Box>
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Title order={6} mb={6}>
<Box> Gambar
<Dropzone </Title>
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' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Idle> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih 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" inline mt={7}>
Maksimal 5MB dan harus format gambar Maksimal 5MB dan harus format gambar
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {previewImage && (
{previewImage && ( <Box mt="sm">
<Box mt="sm"> <Image
<Image src={previewImage}
src={previewImage} alt="Preview"
alt="Preview" style={{
style={{ maxWidth: '100%',
maxWidth: '100%', maxHeight: '200px',
maxHeight: '200px', objectFit: 'contain',
objectFit: 'contain', borderRadius: '8px',
borderRadius: '8px', border: '1px solid #ddd',
border: '1px solid #ddd', }}
}} />
/> </Box>
</Box> )}
)}
</Box>
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan <Group justify="right">
</Button> <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,26 @@
'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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; Box,
import JudulList from '../../_com/judulList'; Button,
Center,
Group,
Image,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan'; import programKesehatan from '../../_state/kesehatan/program-kesehatan/programKesehatan';
@@ -14,9 +32,10 @@ function ProgramKesehatan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header dengan Search */}
<HeaderSearch <HeaderSearch
title='Program Kesehatan' title='Program Kesehatan'
placeholder='pencarian' placeholder='Cari program kesehatan...'
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,88 +46,112 @@ function ProgramKesehatan() {
} }
function ListProgramKesehatan({ search }: { search: string }) { function ListProgramKesehatan({ search }: { search: string }) {
const programKesehatanState = useProxy(programKesehatan) const stateProgram = useProxy(programKesehatan);
const router = useRouter() const router = useRouter();
const { const { data, page, totalPages, loading, load } = stateProgram.findMany;
data,
page,
totalPages,
loading,
load,
} = programKesehatanState.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 (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> {/* Header List + Tombol Tambah */}
<JudulList <Group justify="space-between" mb="md">
title='List Program Kesehatan' <Title order={4}>Daftar Program Kesehatan</Title>
href='/admin/kesehatan/program-kesehatan/create' <Tooltip label="Tambah Program Kesehatan" withArrow>
/> <Button
<Box style={{ overflowX: "auto" }}> leftSection={<IconPlus size={18} />}
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> color="blue"
<TableThead> variant="light"
<TableTr> onClick={() => router.push('/admin/kesehatan/program-kesehatan/create')}
<TableTh w={250}>Judul</TableTh> >
<TableTh w={250}>Deskripsi Singkat</TableTh> Tambah Baru
<TableTh w={250}>Image</TableTh> </Button>
<TableTh w={200}>Detail</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead>
<TableTbody> {/* Tabel */}
{filteredData.map((item) => ( <Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Judul</TableTh>
<TableTh>Deskripsi Singkat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd> <TableTd>
<Box w={100}> <Text fw={500} truncate="end" lineClamp={1}>
<Text truncate="end" fz={"sm"}>{item.name}</Text> {item.name}
</Box> </Text>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Box w={100}> <Text fz="sm" truncate="end" lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} />
</Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image?.link} alt="image" /> <Image w={100} src={item.image?.link} alt="image" radius="md" />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/program-kesehatan/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada program kesehatan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10, search);
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 ProgramKesehatan; export default ProgramKesehatan;

View File

@@ -4,7 +4,18 @@
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
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';
@@ -85,7 +96,6 @@ function EditPuskesmas() {
imageId: form.imageId, imageId: form.imageId,
}); });
// Check if there's an existing image URL in the form data
const formWithImage = form as PuskesmasFormData; const formWithImage = form as PuskesmasFormData;
if (formWithImage.image?.link) { if (formWithImage.image?.link) {
setPreviewImage(formWithImage.image.link); setPreviewImage(formWithImage.image.link);
@@ -105,17 +115,8 @@ function EditPuskesmas() {
...statePuskesmas.edit.form, ...statePuskesmas.edit.form,
name: formData.name, name: formData.name,
alamat: formData.alamat, alamat: formData.alamat,
jam: { jam: { ...formData.jam },
workDays: formData.jam.workDays, kontak: { ...formData.kontak },
weekDays: formData.jam.weekDays,
holiday: formData.jam.holiday,
},
kontak: {
kontakPuskesmas: formData.kontak.kontakPuskesmas,
email: formData.kontak.email,
facebook: formData.kontak.facebook,
kontakUGD: formData.kontak.kontakUGD,
},
imageId: formData.imageId, imageId: formData.imageId,
}; };
@@ -144,166 +145,182 @@ function EditPuskesmas() {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData(prev => ({ setFormData(prev => ({ ...prev, [name]: value }));
...prev,
[name]: value
}));
}; };
const handleNestedChange = (section: 'jam' | 'kontak', field: string, value: string) => { const handleNestedChange = (section: 'jam' | 'kontak', field: string, value: string) => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
[section]: { [section]: { ...prev[section], [field]: value }
...prev[section],
[field]: value
}
})); }));
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan 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 variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Stack gap="xs"> </Button>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> </Tooltip>
<Stack gap="xs"> <Title order={4} ml="sm" c="dark">
<Title order={3}>Edit Puskesmas</Title> Edit Puskesmas
</Title>
</Group>
<TextInput {/* Card Form */}
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>} <Paper
placeholder="masukkan nama puskesmas" w={{ base: '100%', md: '50%' }}
name="name" bg={colors['white-1']}
value={formData.name} p="lg"
onChange={handleInputChange} radius="md"
/> shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Puskesmas"
placeholder="Masukkan nama puskesmas"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" placeholder="Masukkan alamat"
name="alamat" name="alamat"
value={formData.alamat} value={formData.alamat}
onChange={handleInputChange} onChange={handleInputChange}
/> required
/>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>} label="Jam Buka"
placeholder="masukkan jam buka" placeholder="Masukkan jam buka"
value={formData.jam.workDays} value={formData.jam.workDays}
onChange={(e) => handleNestedChange('jam', 'workDays', e.target.value)} onChange={(e) => handleNestedChange('jam', 'workDays', e.target.value)}
/> required
/>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>} label="Jam Tutup"
placeholder="masukkan jam tutup" placeholder="Masukkan jam tutup"
value={formData.jam.weekDays} value={formData.jam.weekDays}
onChange={(e) => handleNestedChange('jam', 'weekDays', e.target.value)} onChange={(e) => handleNestedChange('jam', 'weekDays', e.target.value)}
/> required
/>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Libur</Text>} label="Jam Libur"
placeholder="masukkan jam libur" placeholder="Masukkan jam libur"
value={formData.jam.holiday} value={formData.jam.holiday}
onChange={(e) => handleNestedChange('jam', 'holiday', e.target.value)} onChange={(e) => handleNestedChange('jam', 'holiday', e.target.value)}
/> required
/>
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>} label="Kontak Puskesmas"
placeholder="masukkan kontak puskesmas" placeholder="Masukkan kontak puskesmas"
value={formData.kontak.kontakPuskesmas} value={formData.kontak.kontakPuskesmas}
onChange={(e) => handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'kontakPuskesmas', e.target.value)}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Email</Text>} label="Email"
placeholder="masukkan email" placeholder="Masukkan email"
value={formData.kontak.email} value={formData.kontak.email}
onChange={(e) => handleNestedChange('kontak', 'email', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'email', e.target.value)}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>} label="Facebook"
placeholder="masukkan facebook" placeholder="Masukkan facebook"
value={formData.kontak.facebook} value={formData.kontak.facebook}
onChange={(e) => handleNestedChange('kontak', 'facebook', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'facebook', e.target.value)}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>} label="Kontak UGD"
placeholder="masukkan kontak UGD" placeholder="Masukkan kontak UGD"
value={formData.kontak.kontakUGD} value={formData.kontak.kontakUGD}
onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)} onChange={(e) => handleNestedChange('kontak', 'kontakUGD', e.target.value)}
/> />
<Box> {/* Upload Gambar */}
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Box>
<Box> <Text fz="sm" fw="bold" mb={6}>
<Dropzone Gambar
onDrop={(files) => { </Text>
const selectedFile = files[0]; // Ambil file pertama <Dropzone
if (selectedFile) { onDrop={(files) => {
setFile(selectedFile); const selectedFile = files[0];
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview if (selectedFile) {
} setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={200} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}} }}
onReject={() => toast.error('File tidak valid.')} />
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* 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',
}}
/>
</Box>
)}
</Box> </Box>
</Box> )}
</Box>
<Group justify="right">
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
bg={colors['blue-button']} 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)',
}}
loading={statePuskesmas.edit.loading} loading={statePuskesmas.edit.loading}
> >
Simpan Perubahan Simpan
</Button> </Button>
</Stack> </Group>
</Paper> </Stack>
</Stack> </Paper>
</Box> </Box>
); );
} }

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Flex, Text, Image, Skeleton } from '@mantine/core'; import { Box, Button, Paper, Stack, Text, Image, Skeleton, Group, Tooltip } from '@mantine/core';
import { IconArrowBack, IconX, IconEdit } 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 React, { useState } from 'react'; import React, { useState } from 'react';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
@@ -10,90 +10,128 @@ import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailPuskesmas() { function DetailPuskesmas() {
const params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
const statePuskesmas = useProxy(puskesmasState) const statePuskesmas = useProxy(puskesmasState);
const [modalHapus, setModalHapus] = useState(false); const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null);
useShallowEffect(() => { useShallowEffect(() => {
statePuskesmas.findUnique.load(params?.id as string) statePuskesmas.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
statePuskesmas.delete.byId(selectedId) statePuskesmas.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/kesehatan/puskesmas") router.push("/admin/kesehatan/puskesmas");
} }
} };
if (!statePuskesmas.findUnique.data) { if (!statePuskesmas.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = statePuskesmas.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 w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Puskesmas</Text> Kembali
{statePuskesmas.findUnique.data ? ( </Button>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}> <Paper
<Box> withBorder
<Text fz={"lg"} fw={"bold"}>Nama Puskesmas</Text> w={{ base: "100%", md: "50%" }}
<Text fz={"lg"}>{statePuskesmas.findUnique.data.name}</Text> bg={colors['white-1']}
</Box> p="lg"
<Box> radius="md"
<Text fz={"lg"} fw={"bold"}>Alamat</Text> shadow="sm"
<Text fz={"lg"}>{statePuskesmas.findUnique.data.alamat}</Text> >
</Box> <Stack gap="md">
<Box> <Text fz="2xl" fw="bold" c={colors['blue-button']}>
<Text fz={"lg"} fw={"bold"}>Jam Operasional</Text> Detail Puskesmas
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.workDays}</Text> </Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.weekDays}</Text>
<Text fz={"lg"}>{statePuskesmas.findUnique.data.jam.holiday}</Text> <Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
</Box> <Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Gambar</Text> <Text fz="lg" fw="bold">Nama Puskesmas</Text>
<Image src={statePuskesmas.findUnique.data.image?.link} alt="gambar" /> <Text fz="md" c="dimmed">{data?.name || '-'}</Text>
</Box> </Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Kontak</Text> <Box>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakPuskesmas}</Text> <Text fz="lg" fw="bold">Alamat</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.email}</Text> <Text fz="md" c="dimmed">{data?.alamat || '-'}</Text>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.facebook}</Text> </Box>
<Text fz={"lg"} >{statePuskesmas.findUnique.data.kontak.kontakUGD}</Text>
</Box> <Box>
<Box> <Text fz="lg" fw="bold">Jam Operasional</Text>
<Flex gap={"xs"}> <Text fz="md" c="dimmed">{data?.jam?.workDays || '-'}</Text>
<Button color="red" onClick={() => { <Text fz="md" c="dimmed">{data?.jam?.weekDays || '-'}</Text>
if (statePuskesmas.findUnique.data) { <Text fz="md" c="dimmed">{data?.jam?.holiday || '-'}</Text>
setSelectedId(statePuskesmas.findUnique.data.id) </Box>
setModalHapus(true)
} <Box>
}}> <Text fz="lg" fw="bold">Gambar</Text>
<IconX size={20} /> {data?.image?.link ? (
</Button> <Image src={data.image.link} alt="gambar" radius="md" />
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${statePuskesmas.findUnique.data?.id}/edit`)} color="green"> ) : (
<IconEdit size={20} /> <Text fz="md" c="dimmed">-</Text>
</Button> )}
</Flex> </Box>
</Box>
</Stack> <Box>
</Paper> <Text fz="lg" fw="bold">Kontak</Text>
) : null} <Text fz="md" c="dimmed">{data?.kontak?.kontakPuskesmas || '-'}</Text>
<Text fz="md" c="dimmed">{data?.kontak?.email || '-'}</Text>
<Text fz="md" c="dimmed">{data?.kontak?.facebook || '-'}</Text>
<Text fz="md" c="dimmed">{data?.kontak?.kontakUGD || '-'}</Text>
</Box>
<Group gap="sm">
<Tooltip label="Hapus Data" withArrow position="top">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
</Tooltip>
<Tooltip label="Edit Data" withArrow position="top">
<Button
color="green"
onClick={() =>
router.push(`/admin/kesehatan/puskesmas/${data.id}/edit`)
}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack>
</Paper>
</Stack> </Stack>
</Paper> </Paper>
@@ -102,7 +140,7 @@ function DetailPuskesmas() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?" text="Apakah anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );

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 { 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';
@@ -11,44 +22,39 @@ import { useProxy } from 'valtio/utils';
import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '../../../_state/kesehatan/puskesmas/puskesmas';
function CreatePuskesmas() { function CreatePuskesmas() {
const statePuskesmas = useProxy(puskesmasState) const statePuskesmas = useProxy(puskesmasState);
const router = useRouter(); const router = useRouter();
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null); const [previewImage, setPreviewImage] = useState<string | null>(null);
const resetForm = () => { const resetForm = () => {
statePuskesmas.create.form = { statePuskesmas.create.form = {
name: "", name: '',
alamat: "", alamat: '',
jam: { jam: {
workDays: "", workDays: '',
weekDays: "", weekDays: '',
holiday: "", holiday: '',
}, },
kontak: { kontak: {
kontakPuskesmas: "", kontakPuskesmas: '',
email: "", email: '',
facebook: "", facebook: '',
kontakUGD: "", kontakUGD: '',
}, },
imageId: "", imageId: '',
image: undefined, image: undefined,
}; };
setFile(null); setFile(null);
setPreviewImage(null); setPreviewImage(null);
}; };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!file) { if (!file) {
return toast.warn("Pilih file gambar terlebih dahulu"); return toast.warn('Pilih file gambar terlebih dahulu');
} }
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({ const res = await ApiFetch.api.fileStorage.create.post({
file, file,
name: file.name, name: file.name,
@@ -56,162 +62,171 @@ function CreatePuskesmas() {
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');
} }
statePuskesmas.create.form.imageId = uploaded.id; statePuskesmas.create.form.imageId = uploaded.id;
// State is already being updated directly in the form inputs
await statePuskesmas.create.submit(); await statePuskesmas.create.submit();
toast.success("Data berhasil disimpan"); toast.success('Data berhasil disimpan');
resetForm(); resetForm();
// After successful submission, redirect to the list page
router.push('/admin/kesehatan/puskesmas'); router.push('/admin/kesehatan/puskesmas');
} };
return ( return (
<Box component="form" onSubmit={handleSubmit}> <Box px={{ base: 'sm', md: 'lg' }} py="md" component="form" onSubmit={handleSubmit}>
<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} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Puskesmas
</Title>
</Group>
<Paper bg={colors['white-1']} p="md" w={{ base: '100%', md: '50%' }}> {/* Form Card */}
<Stack gap="xs"> <Paper
<Title order={3}>Create Puskesmas</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
label={<Text fz="sm" fw="bold">Nama Puskesmas</Text>} label="Nama Puskesmas"
placeholder="masukkan nama puskesmas" placeholder="Masukkan nama puskesmas"
value={statePuskesmas.create.form.name} value={statePuskesmas.create.form.name}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.name = e.target.value)}
statePuskesmas.create.form.name = e.target.value; required
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Alamat</Text>} label="Alamat"
placeholder="masukkan alamat" placeholder="Masukkan alamat"
value={statePuskesmas.create.form.alamat} value={statePuskesmas.create.form.alamat}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.alamat = e.target.value)}
statePuskesmas.create.form.alamat = e.target.value; required
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Buka</Text>} label="Jam Buka"
placeholder="masukkan jam buka" placeholder="Masukkan jam buka"
value={statePuskesmas.create.form.jam.workDays} value={statePuskesmas.create.form.jam.workDays}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.jam.workDays = e.target.value)}
statePuskesmas.create.form.jam.workDays = e.target.value;
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Jam Tutup</Text>} label="Jam Tutup"
placeholder="masukkan jam tutup" placeholder="Masukkan jam tutup"
value={statePuskesmas.create.form.jam.weekDays} value={statePuskesmas.create.form.jam.weekDays}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.jam.weekDays = e.target.value)}
statePuskesmas.create.form.jam.weekDays = e.target.value;
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Holiday</Text>} label="Holiday"
placeholder="masukkan holiday" placeholder="Masukkan hari libur"
value={statePuskesmas.create.form.jam.holiday} value={statePuskesmas.create.form.jam.holiday}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.jam.holiday = e.target.value)}
statePuskesmas.create.form.jam.holiday = e.target.value;
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak Puskesmas</Text>} label="Kontak Puskesmas"
placeholder="masukkan kontak puskesmas" placeholder="Masukkan kontak puskesmas"
value={statePuskesmas.create.form.kontak.kontakPuskesmas} value={statePuskesmas.create.form.kontak.kontakPuskesmas}
onChange={(e) => { onChange={(e) =>
statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value; (statePuskesmas.create.form.kontak.kontakPuskesmas = e.target.value)
}} }
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Email</Text>} label="Email"
placeholder="masukkan email" placeholder="Masukkan email"
value={statePuskesmas.create.form.kontak.email} value={statePuskesmas.create.form.kontak.email}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.kontak.email = e.target.value)}
statePuskesmas.create.form.kontak.email = e.target.value;
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Facebook</Text>} label="Facebook"
placeholder="masukkan facebook" placeholder="Masukkan facebook"
value={statePuskesmas.create.form.kontak.facebook} value={statePuskesmas.create.form.kontak.facebook}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.kontak.facebook = e.target.value)}
statePuskesmas.create.form.kontak.facebook = e.target.value;
}}
/> />
<TextInput <TextInput
label={<Text fz="sm" fw="bold">Kontak UGD</Text>} label="Kontak UGD"
placeholder="masukkan kontak ugd" placeholder="Masukkan kontak UGD"
value={statePuskesmas.create.form.kontak.kontakUGD} value={statePuskesmas.create.form.kontak.kontakUGD}
onChange={(e) => { onChange={(e) => (statePuskesmas.create.form.kontak.kontakUGD = e.target.value)}
statePuskesmas.create.form.kontak.kontakUGD = e.target.value;
}}
/> />
<Box> <Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text> <Title order={6} mb={6}>
<Box> Gambar
<Dropzone </Title>
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' }}> >
<Dropzone.Accept> <Group justify="center" gap="xl" mih={200} style={{ pointerEvents: 'none' }}>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} /> <Dropzone.Accept>
</Dropzone.Accept> <IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<Dropzone.Reject> </Dropzone.Accept>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} /> <Dropzone.Reject>
</Dropzone.Reject> <IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<Dropzone.Idle> </Dropzone.Reject>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} /> <Dropzone.Idle>
</Dropzone.Idle> <IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div> <div>
<Text size="xl" inline> <Text size="xl" inline>
Drag gambar ke sini atau klik untuk pilih 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" inline mt={7}>
Maksimal 5MB dan harus format gambar Maksimal 5MB dan harus format gambar
</Text> </Text>
</div> </div>
</Group> </Group>
</Dropzone> </Dropzone>
{/* Tampilkan preview kalau ada */} {previewImage && (
{previewImage && ( <Box mt="sm">
<Box mt="sm"> <Image
<Image src={previewImage}
src={previewImage} alt="Preview"
alt="Preview" style={{
style={{ maxWidth: '100%',
maxWidth: '100%', maxHeight: '200px',
maxHeight: '200px', objectFit: 'contain',
objectFit: 'contain', borderRadius: '8px',
borderRadius: '8px', border: '1px solid #ddd',
border: '1px solid #ddd', }}
}} />
/> </Box>
</Box> )}
)}
</Box>
</Box> </Box>
<Button onClick={handleSubmit} bg={colors['blue-button']}>
Simpan Puskesmas {/* Action Button */}
</Button> <Group justify="right">
<Button
type="submit"
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,105 +1,155 @@
'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 } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Image,
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 { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
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 puskesmasState from '../../_state/kesehatan/puskesmas/puskesmas'; import puskesmasState from '../../_state/kesehatan/puskesmas/puskesmas';
import { useState } from 'react';
function Puskesmas() { function Puskesmas() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
{/* Header dengan Search */}
<HeaderSearch <HeaderSearch
title='Puskesmas' title='Puskesmas'
placeholder='pencarian' placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListPuskesmas search={search} /> <ListPuskesmas search={search} />
</Box> </Box>
); );
} }
function ListPuskesmas({ search }: { search: string }) { function ListPuskesmas({ search }: { search: string }) {
const statePuskesmas = useProxy(puskesmasState) const statePuskesmas = useProxy(puskesmasState);
const router = useRouter(); const router = useRouter();
const { const { data, page, totalPages, loading, load } = statePuskesmas.findMany;
data,
page,
totalPages,
loading,
load,
} = statePuskesmas.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 (
<Box py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Box> </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">
<Stack> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Puskesmas</Title>
title='List Puskesmas' <Tooltip label="Tambah Puskesmas" withArrow>
href='/admin/kesehatan/puskesmas/create' <Button
/> leftSection={<IconPlus size={18} />}
<Box style={{ overflowX: "auto" }}> color="blue"
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> variant="light"
<TableThead> onClick={() => router.push('/admin/kesehatan/puskesmas/create')}
<TableTr> >
<TableTh>Nama Puskesmas</TableTh> Tambah Baru
<TableTh>Alamat</TableTh> </Button>
<TableTh>Image</TableTh> </Tooltip>
<TableTh>Detail</TableTh> </Group>
</TableTr>
</TableThead> <Box style={{ overflowX: "auto" }}>
<TableTbody> <Table highlightOnHover>
{filteredData.map((item) => ( <TableThead>
<TableTr>
<TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Image</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.name}</TableTd> <TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>{item.alamat}</TableTd> <TableTd>{item.alamat}</TableTd>
<TableTd> <TableTd>
<Image w={100} src={item.image.link} alt="image" /> <Image w={100} src={item.image.link} alt="image" radius="md" />
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}> <Button
<IconDeviceImacCog size={25} /> variant="light"
color="blue"
onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}
>
<IconDeviceImacCog size={20} />
<Text ml={5}>Detail</Text>
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} ))
</TableTbody> ) : (
</Table> <TableTr>
</Box> <TableTd colSpan={4}>
</Stack> <Center py={20}>
<Text color="dimmed">Tidak ada data puskesmas yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center> <Center>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} // ini penting! onChange={(newPage) => {
load(newPage, 10, search);
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 Puskesmas; export default Puskesmas;

View File

@@ -66,7 +66,7 @@ function Page() {
</Box> </Box>
<Divider my="md" color={colors['blue-button']} /> <Divider my="md" color={colors['blue-button']} />
<Box px={{ base: 0, md: 50 }} pb="xl"> <Box px={{ base: 0, md: 50 }} pb="xl">
<Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg"> <Paper withBorder bg={"#fff"} radius="md" shadow="xs" p="lg">
<Stack gap={0}> <Stack gap={0}>
<Center> <Center>
<Image <Image

View File

@@ -1,33 +1,33 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Paper, Pagination, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } 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, IconPlus, IconSearch, IconX } from '@tabler/icons-react'; import { IconEdit, IconPlus, IconX } 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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud'; import infoSekolahPaud from '../../../_state/pendidikan/info-sekolah-paud';
function JenjangPendidikan() { function JenjangPendidikan() {
const [search, setSearch] = useState("") // const [search, setSearch] = useState("")
return ( return (
<Box> <Box>
<HeaderSearch <Title order={4}>Jenjang Pendidikan</Title>
{/* <HeaderSearch
title='Jenjang Pendidikan' title='Jenjang Pendidikan'
placeholder='Cari jenjang pendidikan...' placeholder='Cari jenjang pendidikan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> /> */}
<ListJenjangPendidikan search={search} /> <ListJenjangPendidikan />
</Box> </Box>
); );
} }
function ListJenjangPendidikan({ search }: { search: string }) { function ListJenjangPendidikan() {
const stateList = useProxy(infoSekolahPaud.jenjangPendidikan) const stateList = useProxy(infoSekolahPaud.jenjangPendidikan)
const [modalHapus, setModalHapus] = useState(false) const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -50,8 +50,8 @@ function ListJenjangPendidikan({ search }: { search: string }) {
} }
useShallowEffect(() => { useShallowEffect(() => {
load(page, 10, search) load(page, 10)
}, [page, search]) }, [page])
const filteredData = data || [] const filteredData = data || []

View File

@@ -2,7 +2,7 @@
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } 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 { IconSearch, IconTrash } from '@tabler/icons-react'; import { IconCheck, IconSearch, IconX } from '@tabler/icons-react';
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';
@@ -75,9 +75,9 @@ function ListUser({ search }: { search: string }) {
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh style={{ width: '25%' }}>Nama User</TableTh> <TableTh style={{ width: '25%' }}>Nama User</TableTh>
<TableTh style={{ width: '20%' }}>Email</TableTh> <TableTh style={{ width: '20%' }}>Nomor</TableTh>
<TableTh style={{ width: '20%' }}>Role</TableTh> <TableTh style={{ width: '20%' }}>Role</TableTh>
<TableTh style={{ width: '15%' }}>Hapus</TableTh> <TableTh style={{ width: '15%' }}>Aktif / Nonaktif</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
@@ -98,16 +98,19 @@ function ListUser({ search }: { search: string }) {
</Text> </Text>
</TableTd> </TableTd>
<TableTd style={{ width: '15%' }}> <TableTd style={{ width: '15%' }}>
<Tooltip label="Hapus user" withArrow> <Tooltip
label={item.isActive ? "Nonaktifkan user" : "Aktifkan user"}
withArrow
>
<Button <Button
variant="light" variant="light"
color="red" color={item.isActive ? "green" : "red"}
onClick={() => { onClick={async () => {
setSelectedId(item.id) await stateUser.updateActive.submit(item.id, !item.isActive)
setModalHapus(true) stateUser.findMany.load(page, 10, search)
}} }}
> >
<IconTrash size={20} /> {item.isActive ? <IconCheck size={20} /> : <IconX size={20} />}
</Button> </Button>
</Tooltip> </Tooltip>
</TableTd> </TableTd>

View File

@@ -1,9 +1,10 @@
export { export {
apiFetchLogin apiFetchLogin,
apiFetchRegister
}; };
const apiFetchLogin = async ({ nomor }: { nomor: string }) => { const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
const respone = await fetch("/api/auth/login", { const response = await fetch("/api/auth/login", {
method: "POST", method: "POST",
body: JSON.stringify({ nomor: nomor }), body: JSON.stringify({ nomor: nomor }),
headers: { headers: {
@@ -11,5 +12,30 @@ const apiFetchLogin = async ({ nomor }: { nomor: string }) => {
}, },
}); });
return await respone.json().catch(() => null); return await response.json().catch(() => null);
};
const apiFetchRegister = async ({
nomor,
username,
}: {
nomor: string;
username: string;
}) => {
const data = {
username: username,
nomor: nomor,
};
const respone = await fetch("/api/auth/register", {
method: "POST",
body: JSON.stringify({ data }),
headers: {
"Content-Type": "application/json",
},
});
const result = await respone.json();
return result;
// return await respone.json().catch(() => null);
}; };

View File

@@ -1,81 +0,0 @@
'use client'
import BackButton from '@/app/darmasaba/(pages)/desa/layanan/_com/BackButto';
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Group, Image, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconUserFilled } from '@tabler/icons-react';
import Link from 'next/link';
function Page() {
// const router = useRouter()
// const snap = useSnapshot(userState.userState)
// const handleSubmit = async () => {
// router.push("/darmasaba/pendidikan/perpustakaan-digital")
// await snap.login.submit()
// }
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }} >
<Center>
<Image src={"/darmasaba-icon.png"} alt="" w={80} />
</Center>
<Box>
<Title ta={'center'} order={1} fw={'bold'} c={colors['blue-button']}>
E-Book Desa Darmasaba
</Title>
<Text ta={'center'} fz={'h4'} fw={'bold'} c={colors['blue-button']}>
Silahkan masukkan akun anda untuk menjelajahi berbagai macam buku di perpustakaan digital
</Text>
</Box>
</Box>
<Box px={{ base: 'md', md: 100 }} pb={50}>
<Group justify='center'>
<Paper p={'xl'} radius={'md'} bg={colors['white-trans-1']}>
<Stack align='center'>
<Title order={2} fw={'bold'} c={colors['blue-button']}>
Login
</Title>
<IconUserFilled size={80} color={colors['blue-button']} />
<Box>
<Text c={colors['blue-button']} fw={'bold'}>Masuk Untuk Akses Lebih Banyak Buku</Text>
<TextInput
type='email'
label='Email'
placeholder='Email'
// value={snap.login.form.email}
// onChange={(e) => {
// userState.userState.login.form.email = e.target.value
// }}
required
/>
<TextInput py={20}
type='password'
label='Password'
placeholder='Password'
// value={snap.login.form.password}
// onChange={(e) => {
// userState.userState.login.form.password = e.target.value
// }}
/>
<Box pb={20} >
<Button fullWidth bg={colors['blue-button']} radius={'xl'}>Masuk</Button>
</Box>
<Flex justify={'center'} align={'center'}>
<Text>Belum punya akun? </Text>
<Button variant='transparent' component={Link} href={'/registrasi'}>
<Text c={colors['blue-button']} fw={'bold'}>Registrasi</Text>
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Group>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -21,7 +21,7 @@ import { useDisclosure } from "@mantine/hooks";
import { import {
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconDoorExit, IconLogout2
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import _ from "lodash"; import _ from "lodash";
import Link from "next/link"; import Link from "next/link";
@@ -107,7 +107,21 @@ export default function Layout({ children }: { children: React.ReactNode }) {
variant="gradient" variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }} gradient={{ from: colors["blue-button"], to: "#228be6" }}
> >
<IconDoorExit size={22} /> <Image src="/assets/images/darmasaba-icon.png" alt="Logo Darmasaba" w={25} h={25} radius="md" />
</ActionIcon>
</Tooltip>
<Tooltip label="Keluar" position="bottom" withArrow>
<ActionIcon
onClick={() => {
router.push("/login");
}}
color={colors["blue-button"]}
radius="xl"
size="lg"
variant="gradient"
gradient={{ from: colors["blue-button"], to: "#228be6" }}
>
<IconLogout2 size={22} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Group> </Group>

View File

@@ -1,30 +1,64 @@
/* 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 artikelKesehatanFindMany(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 = [
{ title: { contains: search, mode: "insensitive" } },
{ introduction: { content: { contains: search, mode: "insensitive" } } },
{ symptom: { title: { contains: search, mode: "insensitive" } } },
{ prevention: { title: { contains: search, mode: "insensitive" } } },
{ firstaid: { title: { contains: search, mode: "insensitive" } } },
{ mythvsfact: { title: { contains: search, mode: "insensitive" } } },
{ doctorsign: { content: { contains: search, mode: "insensitive" } } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.artikelKesehatan.findMany({
where,
include: {
introduction: true,
symptom: true,
prevention: true,
firstaid: true,
mythvsfact: true,
doctorsign: true,
},
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.artikelKesehatan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil keamanan lingkungan 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 keamanan lingkungan",
};
}
}
export default async function artikelKesehatanFindMany() {
try {
const data = await prisma.artikelKesehatan.findMany({
where: {
isActive: true,
},
include: {
introduction: true,
symptom: true,
prevention: true,
firstaid: true,
mythvsfact: true,
doctorsign: true,
}
})
return {
success: true,
message: "Success fetch artikel kesehatan",
data,
}
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch artikel kesehatan",
}
}
}

View File

@@ -1,11 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function findManyFasilitasKesehatan() { async function fasilitasKesehatanFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.fasilitasKesehatan.findMany({ const page = Number(context.query.page) || 1;
where: { const limit = Number(context.query.limit) || 10;
isActive: true, 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 = [
{ informasiUmum: { fasilitas: { contains: search, mode: 'insensitive' } } },
{ layananUnggulan: { content: { contains: search, mode: 'insensitive' } } },
{ dokterdanTenagaMedis: { name: { contains: search, mode: 'insensitive' } } },
{ fasilitasPendukung: { content: { contains: search, mode: 'insensitive' } } },
{ prosedurPendaftaran: { content: { contains: search, mode: 'insensitive' } } },
{ tarifdanlayanan: { layanan: { contains: search, mode: 'insensitive' } } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.fasilitasKesehatan.findMany({
where,
include: { include: {
informasiumum: true, informasiumum: true,
layananunggulan: true, layananunggulan: true,
@@ -13,18 +37,29 @@ export default async function findManyFasilitasKesehatan() {
fasilitaspendukung: true, fasilitaspendukung: true,
prosedurpendaftaran: true, prosedurpendaftaran: true,
tarifdanlayanan: true, tarifdanlayanan: true,
} },
}) skip,
return { take: limit,
success: true, orderBy: { createdAt: 'desc' },
message: "Success fetch fasilitas kesehatan", }),
data, prisma.fasilitasKesehatan.count({ where }),
} ]);
} catch (error) {
console.error("Find many error:", error); return {
return { success: true,
success: false, message: "Berhasil ambil keamanan lingkungan dengan pagination",
message: "Failed fetch fasilitas kesehatan", 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 keamanan lingkungan",
};
} }
} }
export default fasilitasKesehatanFindMany

View File

@@ -1,30 +1,60 @@
/* 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 jadwalKegiatanFindMany() { export default async function jadwalKegiatanFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.jadwalKegiatan.findMany({ const page = Number(context.query.page) || 1;
where: { const limit = Number(context.query.limit) || 10;
isActive: true, const search = (context.query.search as string) || "";
}, const skip = (page - 1) * limit;
include: {
informasijadwalkegiatan: true, // Buat where clause
deskripsijadwalkegiatan: true, const where: any = { isActive: true };
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true, // Tambahkan pencarian (jika ada)
dokumenjadwalkegiatan: true, if (search) {
pendaftaranjadwalkegiatan: true, where.OR = [
} { informasijadwalkegiatan: { name: { contains: search, mode: "insensitive" } } },
}) { deskripsijadwalkegiatan: { deskripsi: { contains: search, mode: "insensitive" } } },
return { {layananjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
success: true, {syaratketentuanjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
message: "Success fetch jadwal kegiatan", {dokumenjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
data, {pendaftaranjadwalkegiatan: { content: { contains: search, mode: "insensitive" } } },
} ];
} catch (error) { }
console.error("Find many error:", error); try {
return { const [data, total] = await Promise.all([
success: false, prisma.jadwalKegiatan.findMany({
message: "Failed fetch jadwal kegiatan", where,
} include: {
} informasijadwalkegiatan: true,
} deskripsijadwalkegiatan: true,
layananjadwalkegiatan: true,
syaratketentuanjadwalkegiatan: true,
dokumenjadwalkegiatan: true,
pendaftaranjadwalkegiatan: true,
},
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.jadwalKegiatan.count({ where }),
]);
return {
success: true,
message: "Success fetch jadwal kegiatan",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (error) {
console.error("Find many error:", error);
return {
success: false,
message: "Failed fetch jadwal kegiatan",
};
}
}

View File

@@ -4,6 +4,7 @@ import { Elysia, t } from "elysia";
import userFindMany from "./findMany"; import userFindMany from "./findMany";
import userFindUnique from "./findUnique"; import userFindUnique from "./findUnique";
import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts` import userDelete from "./del"; // `delete` nggak boleh jadi nama file JS langsung, jadi biasanya `del.ts`
import userUpdate from "./updt";
const User = new Elysia({ prefix: "/api/user" }) const User = new Elysia({ prefix: "/api/user" })
.get("/findMany", userFindMany) .get("/findMany", userFindMany)
@@ -12,6 +13,7 @@ const User = new Elysia({ prefix: "/api/user" })
params: t.Object({ params: t.Object({
id: t.String(), id: t.String(),
}), }),
}); // pakai PUT untuk soft delete }) // pakai PUT untuk soft delete
.put("/updt", userUpdate);
export default User; export default User;

View File

@@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function userUpdate(context: Context) {
try {
const { id, isActive } = await context.body as { id: string, isActive: boolean };
if (!id) {
return {
success: false,
message: "ID user wajib ada",
};
}
const updatedUser = await prisma.user.update({
where: { id },
data: { isActive },
select: {
id: true,
username: true,
nomor: true,
isActive: true,
updatedAt: true,
}
});
return {
success: true,
message: `User berhasil ${isActive ? "diaktifkan" : "dinonaktifkan"}`,
data: updatedUser,
};
} catch (e: any) {
console.error("Error update user:", e);
return {
success: false,
message: "Gagal mengupdate status user",
};
}
}

View File

@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { jwtVerify } from "jose";
export async function decrypt({
token,
encodedKey,
}: {
token: string;
encodedKey: string;
}): Promise<Record<string, any> | null> {
if (!token || !encodedKey) {
console.error("Missing required parameters:", {
hasToken: !!token,
hasEncodedKey: !!encodedKey,
});
return null;
}
try {
const enc = new TextEncoder().encode(encodedKey);
const { payload } = await jwtVerify(token, enc, {
algorithms: ["HS256"],
});
if (!payload || !payload.user) {
console.error("Invalid payload structure:", {
hasPayload: !!payload,
hasUser: payload ? !!payload.user : false,
});
return null;
}
// Logging untuk debug
// console.log("Decrypt successful:", {
// payloadExists: !!payload,
// userExists: !!payload.user,
// tokenPreview: token.substring(0, 10) + "...",
// });
return payload.user as Record<string, any>;
} catch (error) {
console.error("Token verification failed:", {
error,
tokenLength: token?.length,
errorName: error instanceof Error ? error.name : "Unknown error",
errorMessage: error instanceof Error ? error.message : String(error),
});
return null;
}
}

View File

@@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SignJWT } from "jose";
export async function encrypt({
user,
exp = "7 year",
encodedKey,
}: {
user: Record<string, any>;
exp?: string;
encodedKey: string;
}): Promise<string | null> {
try {
const enc = new TextEncoder().encode(encodedKey);
return new SignJWT({ user })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(exp)
.sign(enc);
} catch (error) {
console.error("Gagal mengenkripsi", error);
return null;
}
}
// wibu:0.2.82

View File

@@ -0,0 +1,13 @@
"use server";
import prisma from "@/lib/prisma";
export async function auth_getCodeOtpByNumber({kodeId}: {kodeId: string}) {
const data = await prisma.kodeOtp.findFirst({
where: {
id: kodeId,
},
});
return data;
}

View File

@@ -0,0 +1,4 @@
export function randomOTP() {
const random = Math.floor(Math.random() * (9000 - 1000 )) + 1000
return random;
}

View File

@@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { cookies } from "next/headers";
import { encrypt } from "./encrypt";
export async function sessionCreate({
sessionKey,
exp = "7 year",
encodedKey,
user,
}: {
sessionKey: string;
exp?: string;
encodedKey: string;
user: Record<string, unknown>;
}) {
const token = await encrypt({
exp,
encodedKey,
user,
});
const cookie: any = {
key: sessionKey,
value: token,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
},
};
(await cookies()).set(cookie.key, cookie.value, { ...cookie.options });
return token;
}
// wibu:0.2.82

View File

@@ -0,0 +1,63 @@
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { randomOTP } from "../_lib/randomOTP";
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
{ success: false, message: "Method Not Allowed" },
{ status: 405 }
);
}
try {
const codeOtp = randomOTP();
const body = await req.json();
const { nomor } = body;
const res = await fetch(
`https://wa.wibudev.com/code?nom=${nomor}&text=Website Desa Darmasaba - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun Admin lainnya.
\n
>> Kode OTP anda: ${codeOtp}.
`
);
const sendWa = await res.json();
if (sendWa.status !== "success")
return NextResponse.json(
{ success: false, message: "Nomor Whatsapp Tidak Aktif" },
{ status: 400 }
);
const createOtpId = await prisma.kodeOtp.create({
data: {
nomor: nomor,
otp: codeOtp,
},
});
if (!createOtpId)
return NextResponse.json(
{ success: false, message: "Gagal mengirim kode OTP" },
{ status: 400 }
);
return NextResponse.json(
{
success: true,
message: "Kode verifikasi terkirim",
kodeId: createOtpId.id,
},
{ status: 200 }
);
} catch (error) {
console.log("Error Login", error);
return NextResponse.json(
{ success: false, message: "Terjadi masalah saat login" , reason: error as Error },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

10
src/app/login/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { Box } from "@mantine/core";
import Login from "../admin/(dashboard)/auth/login-admin/page";
export default function Page() {
return (
<Box>
<Login />
</Box>
)
}

View File

@@ -0,0 +1,12 @@
import React from 'react';
import Registrasi from '../admin/(dashboard)/auth/registrasi-admin/page';
function Page() {
return (
<div>
<Registrasi/>
</div>
);
}
export default Page;

View File

@@ -0,0 +1,9 @@
import Validasi from "../admin/(dashboard)/auth/validasi-admin/page";
export default function Page() {
return (
<div>
<Validasi />
</div>
);
}