Sinkronisasi UI & API Admin - User Submenu Program Kesehatan

This commit is contained in:
2025-08-18 17:14:33 +08:00
parent bcc51aec12
commit 1e154ced86
11 changed files with 665 additions and 331 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -20,17 +21,43 @@ const defaultForm = {
const programKesehatan = proxy({ const programKesehatan = proxy({
findMany: { findMany: {
data: [] as Prisma.ProgramKesehatanGetPayload<{ data: null as
include: { | Prisma.ProgramKesehatanGetPayload<{
image: true; include: {
}; image: true;
}>[], };
async load() { }>[]
const res = await ApiFetch.api.kesehatan.programkesehatan[ | null,
"find-many" page: 1,
].get(); totalPages: 1,
if (res.status === 200) { loading: false,
programKesehatan.findMany.data = res.data?.data ?? []; search: "",
load: async (page = 1, limit = 10, search = "") => {
programKesehatan.findMany.loading = true; // ✅ Akses langsung via nama path
programKesehatan.findMany.page = page;
programKesehatan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.programkesehatan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKesehatan.findMany.data = res.data.data ?? [];
programKesehatan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
programKesehatan.findMany.data = [];
programKesehatan.findMany.totalPages = 1;
} finally {
programKesehatan.findMany.loading = false;
} }
}, },
}, },
@@ -97,12 +124,15 @@ const programKesehatan = proxy({
try { try {
programKesehatan.delete.loading = true; programKesehatan.delete.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/del/${id}`, { const response = await fetch(
method: "DELETE", `/api/kesehatan/programkesehatan/del/${id}`,
headers: { {
"Content-Type": "application/json", method: "DELETE",
}, headers: {
}); "Content-Type": "application/json",
},
}
);
const result = await response.json(); const result = await response.json();
if (response.ok && result?.success) { if (response.ok && result?.success) {
@@ -156,57 +186,70 @@ const programKesehatan = proxy({
} }
} catch (error) { } catch (error) {
console.error("Error fetching program kesehatan:", error); console.error("Error fetching program kesehatan:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data"); toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null; return null;
} }
}, },
async update() { async update() {
const cek = templateForm.safeParse(programKesehatan.edit.form); const cek = templateForm.safeParse(programKesehatan.edit.form);
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
.join("\n")}] required`; .join("\n")}] required`;
return toast.error(err); return toast.error(err);
} }
try { try {
programKesehatan.edit.loading = true; programKesehatan.edit.loading = true;
const response = await fetch(`/api/kesehatan/programkesehatan/${this.id}`, { const response = await fetch(
method: "PUT", `/api/kesehatan/programkesehatan/${this.id}`,
headers: { {
"Content-Type": "application/json", method: "PUT",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
name: this.form.name, },
deskripsiSingkat: this.form.deskripsiSingkat, body: JSON.stringify({
deskripsi: this.form.deskripsi, name: this.form.name,
imageId: this.form.imageId, deskripsiSingkat: this.form.deskripsiSingkat,
}), deskripsi: this.form.deskripsi,
}); imageId: this.form.imageId,
if (!response.ok) { }),
const errorData = await response.json().catch(() => ({})); }
throw new Error(errorData.message || `HTTP error! status: ${response.status}`); );
} if (!response.ok) {
const result = await response.json(); const errorData = await response.json().catch(() => ({}));
if (result.success) { throw new Error(
toast.success(result.message || "Program kesehatan berhasil diupdate"); errorData.message || `HTTP error! status: ${response.status}`
await programKesehatan.findMany.load(); );
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat mengupdate program kesehatan");
return false;
} finally {
programKesehatan.edit.loading = false;
} }
const result = await response.json();
if (result.success) {
toast.success(
result.message || "Program kesehatan berhasil diupdate"
);
await programKesehatan.findMany.load();
return true;
} else {
throw new Error(result.message || "Gagal update program kesehatan");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat mengupdate program kesehatan"
);
return false;
} finally {
programKesehatan.edit.loading = false;
}
}, },
reset() { reset() {
programKesehatan.edit.id = ""; programKesehatan.edit.id = "";
programKesehatan.edit.form = { ...defaultForm }; programKesehatan.edit.form = { ...defaultForm };
}, },
}, },
}); });

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch"; import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -163,13 +164,43 @@ const puskesmasState = proxy({
}, },
findMany: { findMany: {
data: null as Prisma.PuskesmasGetPayload<{ data: null as
include: { image: true; jam: true; kontak: true }; | Prisma.PuskesmasGetPayload<{
}>[] | null, include: {
async load() { image: true;
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get(); jam: true;
if (res.status === 200) { kontak: true;
puskesmasState.findMany.data = res.data?.data ?? []; };
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
puskesmasState.findMany.loading = true; // ✅ Akses langsung via nama path
puskesmasState.findMany.page = page;
puskesmasState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.kesehatan.puskesmas["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
puskesmasState.findMany.data = res.data.data ?? [];
puskesmasState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
puskesmasState.findMany.data = [];
puskesmasState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch berita paginated:", err);
puskesmasState.findMany.data = [];
puskesmasState.findMany.totalPages = 1;
} finally {
puskesmasState.findMany.loading = false;
} }
}, },
}, },

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core'; import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
@@ -21,7 +21,7 @@ function ProgramKesehatan() {
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListProgramKesehatan search={search}/> <ListProgramKesehatan search={search} />
</Box> </Box>
); );
} }
@@ -30,19 +30,21 @@ function ListProgramKesehatan({ search }: { search: string }) {
const programKesehatanState = useProxy(programKesehatan) const programKesehatanState = useProxy(programKesehatan)
const router = useRouter() const router = useRouter()
const {
data,
page,
totalPages,
loading,
load,
} = programKesehatanState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
programKesehatanState.findMany.load() load(page, 3, search)
}, []) }, [page, search])
const filteredData = (programKesehatanState.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.deskripsiSingkat.toLowerCase().includes(keyword)
);
});
if (!programKesehatanState.findMany.data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py={10}>
<Skeleton h={500} /> <Skeleton h={500} />
@@ -77,7 +79,9 @@ function ListProgramKesehatan({ search }: { search: string }) {
</Box> </Box>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate="end" fz={"sm"} dangerouslySetInnerHTML={{ __html: item.deskripsiSingkat }} /> <Box w={100}>
<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" />
@@ -94,6 +98,15 @@ function ListProgramKesehatan({ search }: { search: string }) {
</Box> </Box>
</Stack> </Stack>
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box> </Box>
) )
} }

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core'; import { Box, Button, Center, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -21,7 +21,7 @@ function Puskesmas() {
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListPuskesmas search={search}/> <ListPuskesmas search={search} />
</Box> </Box>
); );
} }
@@ -30,64 +30,75 @@ function ListPuskesmas({ search }: { search: string }) {
const statePuskesmas = useProxy(puskesmasState) const statePuskesmas = useProxy(puskesmasState)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = statePuskesmas.findMany;
useShallowEffect(() => { useShallowEffect(() => {
statePuskesmas.findMany.load() load(page, 3, search)
}, []) }, [page, search])
const filteredData = (statePuskesmas.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.alamat.toLowerCase().includes(keyword)
);
});
if (!statePuskesmas.findMany.data) { if (loading || !data) {
return ( return (
<Box py={10}> <Box py={10}>
<Skeleton h={500}/> <Skeleton h={500} />
</Box> </Box>
) )
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper bg={colors['white-1']} p={'md'}>
<Stack> <Stack>
<JudulList <JudulList
title='List Puskesmas' title='List Puskesmas'
href='/admin/kesehatan/puskesmas/create' href='/admin/kesehatan/puskesmas/create'
/> />
<Box style={{ overflowX: "auto" }}> <Box style={{ overflowX: "auto" }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}> <Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Nama Puskesmas</TableTh> <TableTh>Nama Puskesmas</TableTh>
<TableTh>Alamat</TableTh> <TableTh>Alamat</TableTh>
<TableTh>Image</TableTh> <TableTh>Image</TableTh>
<TableTh>Detail</TableTh> <TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{item.alamat}</TableTd>
<TableTd>
<Image w={100} src={item.image.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.map((item) => (
</Box> <TableTr key={item.id}>
</Stack> <TableTd>{item.name}</TableTd>
</Paper> <TableTd>{item.alamat}</TableTd>
</Box> <TableTd>
<Image w={100} src={item.image.link} alt="image" />
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/kesehatan/puskesmas/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Box>
) )
} }

View File

@@ -1,25 +1,56 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// /api/berita/findManyPaginated.ts
import prisma from "@/lib/prisma"; import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function programKesehatanFindMany() { async function programKesehatanFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.programKesehatan.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: {
image: true, // Buat where clause
} const where: any = { isActive: true };
})
return { // Tambahkan pencarian (jika ada)
success: true, if (search) {
message: "Success fetch program kesehatan", where.OR = [
data, { name: { contains: search, mode: 'insensitive' } }
} ];
} catch (error) { }
console.error("Find many error:", error);
return { try {
success: false, // Ambil data dan total count secara paralel
message: "Failed fetch program kesehatan", const [data, total] = await Promise.all([
} prisma.programKesehatan.findMany({
} where,
} include: {
image: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.programKesehatan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil program kesehatan 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 program kesehatan",
};
}
}
export default programKesehatanFindMany;

View File

@@ -1,27 +1,59 @@
/* 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 puskesmasFindMany() { async function puskesmasFindMany(context: Context) {
try { // Ambil parameter dari query
const data = await prisma.puskesmas.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: {
image: true, // Buat where clause
jam: true, const where: any = { isActive: true };
kontak: true,
} // Tambahkan pencarian (jika ada)
}) if (search) {
return { where.OR = [
success: true, { name: { contains: search, mode: 'insensitive' } },
message: "Success fetch puskesmas", { alamat: { contains: search, mode: 'insensitive' } },
data, ];
} }
} catch (error) {
console.error("Find many error:", error); try {
return { // Ambil data dan total count secara paralel
success: false, const [data, total] = await Promise.all([
message: "Failed fetch puskesmas", prisma.puskesmas.findMany({
} where,
} include: {
} image: true,
jam: true,
kontak: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.puskesmas.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil puskesmas 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 berita",
};
}
}
export default puskesmasFindMany;

View File

@@ -1,7 +0,0 @@
import { Stack } from "@mantine/core";
export default function Page() {
return <Stack>
kesehatan
</Stack>
}

View File

@@ -0,0 +1,66 @@
'use client'
import programKesehatan from '@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan';
import colors from '@/con/colors';
import { Box, Center, Group, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconCalendar, IconUser } from '@tabler/icons-react';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
function Page() {
const state = useProxy(programKesehatan)
const params = useParams()
useShallowEffect(() => {
state.findUnique.load(params.id as string)
}, [params.id])
if (!state.findUnique.data) {
return (
<div>
<Skeleton h={500} />
</div>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Paper px={{ base: 'md', md: 100 }} radius={10} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}>
<Center my={20}>
<Image radius={"lg"} src={state.findUnique.data.image?.link} alt="" />
</Center>
<Box px={'lg'}>
<Box>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{state.findUnique.data.name}
</Text>
<Text ta={'justify'} fz={'h4'} dangerouslySetInnerHTML={{ __html: state.findUnique.data.deskripsi }}></Text>
</Box>
<Group py={20}>
<Group gap="xs">
<IconCalendar size={18} />
<Text size="sm">
{state.findUnique.data.createdAt ? new Date(state.findUnique.data.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
}) : 'No date available'}
</Text>
</Group>
<Group gap="xs">
<IconUser size={18} />
<Text size="sm">Admin Desa</Text>
</Group>
</Group>
</Box>
</Stack>
</Paper>
</Stack>
);
}
export default Page;

View File

@@ -1,28 +1,14 @@
'use client'
import colors from "@/con/colors"; import colors from "@/con/colors";
import { Box, Button, Center, Group, Image, Paper, SimpleGrid, Stack, Text } from "@mantine/core"; import { Box, Button, Center, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from "@mantine/core";
import { IconBarbell, IconCalendar, IconOld, IconUser, IconUsersGroup } from "@tabler/icons-react"; import { IconBarbell, IconCalendar, IconOld, IconSearch, IconUser, IconUsersGroup } from "@tabler/icons-react";
import BackButton from "../../desa/layanan/_com/BackButto"; import BackButton from "../../desa/layanan/_com/BackButto";
import { useProxy } from "valtio/utils";
import programKesehatan from "@/app/admin/(dashboard)/_state/kesehatan/program-kesehatan/programKesehatan";
import { useState } from "react";
import { useShallowEffect } from "@mantine/hooks";
import { useRouter } from "next/navigation";
const data1 = [
{
id: 1,
judul: 'Posyandu Terintegrasi',
image: '/api/img/pk-1.png',
deskripsi: 'Program pemantauan kesehatan terpadu untuk balita, ibu hamil, dan lansia di Banjar Gulingan dengan sistem pencatatan digital. Layanan meliputi penimbangan, imunisasi, dan konsultasi kesehatan'
},
{
id: 2,
judul: 'Senam Lansia',
image: '/api/img/pk-2.png',
deskripsi: 'Kegiatan olahraga teratur untuk warga lanjut usia dengan gerakan yang disesuaikan untuk menjaga kebugaran dan kesehatan. Program ini didampingi oleh instruktur profesional dan pemantauan kesehatan rutin.'
},
{
id: 3,
judul: 'Vaksinasi & Sterilasi HPR',
image: '/api/img/pk-3.png',
deskripsi: 'Program pengendalian hewan penular rabies melalui vaksinasi dan sterilisasi untuk mencegah penyebaran penyakit zoonosis. Dilengkapi dengan sistem pendataan digital untuk memantau cakupan dan efektivitas program.'
}
]
const data2 = [ const data2 = [
{ {
id: 1, id: 1,
@@ -44,21 +30,49 @@ const data2 = [
}, },
] ]
export default function Page() { export default function Page() {
const state = useProxy(programKesehatan)
const router = useRouter()
const [search, setSearch] = useState('')
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Box> <Grid px={{ base: 'md', md: 100 }} align="center">
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <GridCol span={{ base: 12, md: 9 }}>
Program Kesehatan Unggulan <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
</Text> Program Kesehatan Unggulan
<Text px={{base: 20, md: 90}} ta={"center"} fz={{ base: "h4", md: "h3" }} > </Text>
Desa Darmasaba mengembangkan berbagai program kesehatan terpadu untuk meningkatkan kualitas </GridCol>
hidup masyarakat, dengan pendekatan preventif dan promotif bebrbasis teknologi serta prtisipasi aktif <GridCol span={{ base: 12, md: 3 }}>
warga. <TextInput
</Text> placeholder='Cari Program Kesehatan'
</Box> value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <Stack gap={'lg'}>
<SimpleGrid <SimpleGrid
@@ -67,27 +81,33 @@ export default function Page() {
base: 1, base: 1,
md: 3, md: 3,
}}> }}>
{data1.map((v, k) => { {data.map((v, k) => {
return ( return (
<Paper radius={10} key={k} bg={colors["white-trans-1"]}> <Paper radius={10} key={k} bg={colors["white-trans-1"]}>
<Stack gap={'xs'}> <Stack gap={'xs'}>
<Center> <Center>
<Image src={v.image} alt="" style={{ borderRadius: '14px 14px 0 0' }} /> <Image src={v.image?.link} alt="" style={{ borderRadius: '14px 14px 0 0' }} />
</Center> </Center>
<Box px={'lg'}> <Box px={'lg'}>
<Box> <Box>
<Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}> <Text pb={10} c={colors["blue-button"]} fw={"bold"} fz={"h3"}>
{v.judul} {v.name}
</Text> </Text>
<Text ta={'justify'} fz={'h4'}>{v.deskripsi}</Text> <Text ta={'justify'} fz={'h4'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
</Box> </Box>
<Box py={15}> <Box py={15} onClick={() => router.push(`/darmasaba/kesehatan/program-kesehatan/${v.id}`)}>
<Button fw={'bold'} fz={'h5'} c={colors["blue-button"]} bg={colors["BG-trans"]}>Detail Program</Button> <Button fw={'bold'} fz={'h5'} c={colors["blue-button"]} bg={colors["BG-trans"]}>Detail Program</Button>
</Box> </Box>
<Group py={20}> <Group py={20}>
<Group gap="xs"> <Group gap="xs">
<IconCalendar size={18} /> <IconCalendar size={18} />
<Text size="sm">Selasa, 11 Januari 2025</Text> <Text size="sm">
{v.createdAt ? new Date(v.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
}) : 'No date available'}
</Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<IconUser size={18} /> <IconUser size={18} />
@@ -100,6 +120,15 @@ export default function Page() {
) )
})} })}
</SimpleGrid> </SimpleGrid>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Stack> </Stack>
</Box> </Box>
<Box py={10} px={{ base: "md", md: 100 }}> <Box py={10} px={{ base: "md", md: 100 }}>
@@ -141,6 +170,5 @@ export default function Page() {
</SimpleGrid> </SimpleGrid>
</Box> </Box>
</Stack> </Stack>
) )
} }

View File

@@ -0,0 +1,114 @@
'use client'
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors';
import { BackgroundImage, Box, Grid, GridCol, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useParams } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import BackButton from '../../../desa/layanan/_com/BackButto';
function Page() {
const state = useProxy(puskesmasState)
const params = useParams()
useShallowEffect(() => {
state.findUnique.load(params.id as string)
}, [])
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Stack gap={'lg'} px={{ base: 'md', md: 100 }}>
<Box>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Box pb={30}>
<BackgroundImage
pb={30}
radius={16}
h={{ base: 250, md: 500 }}
src={state.findUnique.data.image.link}
style={{ position: 'relative' }}
>
<Box
style={{
borderRadius: 16,
zIndex: 0
}}
pos={"absolute"}
w={"100%"}
h={"100%"}
bg={colors.trans.dark[2]}
/>
<Text style={{
position: 'absolute',
bottom: 35,
left: 15,
}} fw={'bold'} fz={{ base: 'md', md: 'h3' }} c={colors['white-1']}>{state.findUnique.data.name}</Text>
<Text style={{
position: 'absolute',
bottom: 10,
left: 15,
}} fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['white-1']}>{state.findUnique.data.alamat}</Text>
</BackgroundImage>
<Grid
py={20}>
<GridCol span={{ base: 12, md: 6 }}>
<Box>
<Stack>
<Title order={3}>Informasi</Title>
<Box>
<Text>Alamat: {state.findUnique.data.alamat}</Text>
<Text>Telepon: {state.findUnique.data.kontak.kontakPuskesmas}</Text>
<Text>Email: {state.findUnique.data.kontak.email}</Text>
</Box>
<Title order={3}>Jam Operasional</Title>
<Box>
<Text pb={10} fz={'h4'} fw={"bold"}>
Senin - Kamis: <Text span fz={'h4'}>{state.findUnique.data?.jam.workDays} - {state.findUnique.data?.jam.weekDays}</Text>
</Text>
</Box>
</Stack>
</Box>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Box>
<Paper p={"xl"} bg={'#B1C5F2'}>
<SimpleGrid
cols={{
base: 1,
md: 2
}}>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fw={"bold"} fz={'h3'} ta={'center'}>Poliklinik Umum</Text>
<Text ta={'center'} fz={{ base: 45, md: 65 }} fw={'bold'} c={colors['blue-button']}>26</Text>
</Paper>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text ta={'center'} fz={'h3'} fw={"bold"}>Poli Gigi</Text>
<Text ta={'center'} fz={{ base: 45, md: 65 }} fw={'bold'} c={colors['blue-button']}>26</Text>
</Paper>
</SimpleGrid>
</Paper>
</Box>
</GridCol>
<Box>
</Box>
</Grid>
</Box>
</Paper>
</Box>
</Stack>
</Stack>
);
}
export default Page;

View File

@@ -1,125 +1,97 @@
'use client'
import puskesmasState from '@/app/admin/(dashboard)/_state/kesehatan/puskesmas/puskesmas';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Stack, Box, Paper, Text, BackgroundImage, SimpleGrid, Title, Flex } from '@mantine/core'; import { Anchor, Box, Center, Grid, GridCol, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import React from 'react'; import { useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto'; import BackButton from '../../desa/layanan/_com/BackButto';
function Page() { function Page() {
const state = useProxy(puskesmasState)
const [search, setSearch] = useState('')
const {
data,
page,
totalPages,
loading,
load,
} = state.findMany;
useShallowEffect(() => {
load(page, 3, search)
}, [page, search])
if (loading || !data) {
return (
<Box py={10}>
<Skeleton h={500} />
</Box>
)
}
return ( return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}> <Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}> <Box px={{ base: 'md', md: 100 }}>
<BackButton /> <BackButton />
</Box> </Box>
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}> <Grid align='center' px={{ base: 'md', md: 100 }}>
Puskesmas Darmasaba <GridCol span={{ base: 12, md: 9 }}>
</Text> <Text fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
Puskesmas Darmasaba
</Text>
</GridCol>
<GridCol span={{ base: 12, md: 3 }}>
<TextInput
radius={"lg"}
placeholder='Cari Puskesmas'
value={search}
onChange={(e) => setSearch(e.target.value)}
leftSection={<IconSearch size={20} />}
w={{ base: "50%", md: "100%" }}
/>
</GridCol>
</Grid>
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'}> <SimpleGrid
<Box> cols={{
<Paper p={"xl"} bg={colors['white-trans-1']}> base: 1,
<Box pb={30}> md: 3,
<BackgroundImage }}
pb={30} >
radius={16} {data.map((v, k) => {
h={{ base: 250, md: 500 }} return (
src='/api/img/posyandu.png' <Paper p={"xl"} bg={colors['white-trans-1']} key={k}>
style={{ position: 'relative' }} <Stack gap={"xs"}>
> <Text fw={"bold"} fz={"h3"}>{v.name}</Text>
<Box <Image
style={{ src={v.image.link}
borderRadius: 16, alt={v.name}
zIndex: 0
}}
pos={"absolute"}
w={"100%"}
h={"100%"}
bg={colors.trans.dark[2]}
/> />
<Text style={{
position: 'absolute',
bottom: 35,
left: 15,
}} fw={'bold'} fz={{ base: 'md', md: 'h3' }} c={colors['white-1']}>Puskesmas Darmasaba</Text>
<Text style={{
position: 'absolute',
bottom: 10,
left: 15,
}} fw={'bold'} fz={{ base: 'md', md: 'h4' }} c={colors['white-1']}>Jl. Raya Darmasaba No.45, Badung, Bali</Text>
</BackgroundImage>
<SimpleGrid
py={20}
cols={{
base: 1,
md: 2
}}>
<Box> <Box>
<Stack> <Text>Alamat: {v.alamat}</Text>
<Title order={3}>Jam Operasional</Title> <Text>Telepon: {v.kontak.kontakPuskesmas}</Text>
<Box> <Text>Email: {v.kontak.email}</Text>
<Flex justify={'space-between'} align={'center'}>
<Box>
<Text>Senin - Kamis</Text>
<Text>Jumat</Text>
<Text>Sabtu</Text>
<Text> Minggu & Libur</Text>
</Box>
<Box>
<Text>08:00 - 15:00 WITA</Text>
<Text>08:00 - 15:00 WITA</Text>
<Text>08:00 - 15:00 WITA</Text>
<Text>Tutp (UGD 24 JAM)</Text>
</Box>
</Flex>
</Box>
</Stack>
</Box> </Box>
<Box> <Anchor c={colors['blue-button']} href={`/darmasaba/kesehatan/puskesmas/${v.id}`}>Lihat Detail &gt;</Anchor>
<Stack> </Stack>
<Title order={3}>Jam Operasional</Title> </Paper>
<Box> )
<Flex justify={'space-between'} align={'center'}> })}
<Box> </SimpleGrid>
<Text>Senin - Kamis</Text>
<Text>Jumat</Text>
<Text>Sabtu</Text>
<Text> Minggu & Libur</Text>
</Box>
<Box>
<Text>08:00 - 15:00 WITA</Text>
<Text>08:00 - 15:00 WITA</Text>
<Text>08:00 - 15:00 WITA</Text>
<Text>Tutp (UGD 24 JAM)</Text>
</Box>
</Flex>
</Box>
</Stack>
</Box>
<Box>
</Box>
</SimpleGrid>
</Box>
<Box>
<Paper p={"xl"} bg={'#B1C5F2'}>
<SimpleGrid
cols={{
base: 1,
md: 2
}}>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text fw={"bold"} fz={'h3'} ta={'center'}>Poliklinik Umum</Text>
<Text ta={'center'} fz={{ base: 45, md: 65 }} fw={'bold'} c={colors['blue-button']}>26</Text>
</Paper>
<Paper p={"xl"} bg={colors['white-trans-1']}>
<Text ta={'center'} fz={'h3'} fw={"bold"}>Poli Gigi</Text>
<Text ta={'center'} fz={{ base: 45, md: 65 }} fw={'bold'} c={colors['blue-button']}>26</Text>
</Paper>
</SimpleGrid>
</Paper>
</Box>
</Paper>
</Box>
</Stack>
</Box> </Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)} // ini penting!
total={totalPages}
mt="md"
mb="md"
/>
</Center>
</Stack> </Stack>
); );
} }