refactor(ui): sesuaikan UI balita & ibu-hamil dengan pola penghargaan
- Gunakan HeaderSearch + dua-komponen pattern (outer + inner list)
- Ganti Loader → Skeleton h={600}, ActionIcon → Button size="xs" variant="light"
- Tambah Paper wrapper, layout="fixed" table, desktop/mobile responsive split
- Search debounce 1000ms via useDebouncedValue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -190,7 +190,7 @@ export default function Validasi() {
|
||||
case 2:
|
||||
return '/admin/landing-page/profil/program-inovasi';
|
||||
case 3:
|
||||
return '/admin/kesehatan/posyandu';
|
||||
return '/admin/kesehatan/posyandu/list-posyandu';
|
||||
case 4:
|
||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
default:
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import balitaState from '@/app/admin/(dashboard)/_state/kesehatan/balita/balita';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Loader,
|
||||
Pagination,
|
||||
Paper,
|
||||
Select,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import balitaState from '../../../_state/kesehatan/balita/balita';
|
||||
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
|
||||
const STUNTING_COLORS: Record<string, string> = {
|
||||
NORMAL: 'green',
|
||||
@@ -29,161 +36,272 @@ const STUNTING_COLORS: Record<string, string> = {
|
||||
STUNTING: 'red',
|
||||
};
|
||||
|
||||
export default function BalitaPage() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(balitaState);
|
||||
function BalitaPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Balita Terdaftar"
|
||||
placeholder="Cari nama / NIK / ortu..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListBalita search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListBalita({ search }: { search: string }) {
|
||||
const state = useProxy(balitaState);
|
||||
const router = useRouter();
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
state.findMany.load(1, 10, search, statusFilter);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSearch = () => {
|
||||
state.findMany.load(1, 10, search, statusFilter);
|
||||
};
|
||||
load(page, 10, debouncedSearch, statusFilter);
|
||||
}, [page, debouncedSearch, statusFilter]);
|
||||
|
||||
const handleDelete = async (id: string, nama: string) => {
|
||||
if (!confirm(`Hapus data balita "${nama}"?`)) return;
|
||||
await state.delete.byId(id);
|
||||
};
|
||||
|
||||
const rows = state.findMany.data?.map((d) => (
|
||||
<Table.Tr key={d.id}>
|
||||
<Table.Td>{d.nama}</Table.Td>
|
||||
<Table.Td>{d.jenisKelamin}</Table.Td>
|
||||
<Table.Td>
|
||||
{d.tanggalLahir
|
||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={d.imunisasiLengkap ? 'green' : 'red'} variant="light">
|
||||
{d.imunisasiLengkap ? 'Lengkap' : 'Belum'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={d.giziBaik ? 'green' : 'orange'} variant="light">
|
||||
{d.giziBaik ? 'Baik' : 'Kurang'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={d.pemeriksaanRutin ? 'green' : 'orange'} variant="light">
|
||||
{d.pemeriksaanRutin ? 'Rutin' : 'Tidak'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={STUNTING_COLORS[d.statusStunting] ?? 'gray'} variant="light">
|
||||
{d.statusStunting}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/kesehatan/posyandu/balita/edit/${d.id}`)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="md">
|
||||
<Skeleton h={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={3} c="black">Balita Terdaftar</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/balita/create')}
|
||||
radius="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Group>
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Title order={4}>List Balita</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/balita/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group mb="md" gap="sm">
|
||||
<TextInput
|
||||
placeholder="Cari nama / NIK / ortu..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
radius="md"
|
||||
style={{ flex: 1, maxWidth: 300 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter stunting"
|
||||
data={[
|
||||
{ value: '', label: 'Semua' },
|
||||
{ value: 'NORMAL', label: 'Normal' },
|
||||
{ value: 'ALERT', label: 'Alert' },
|
||||
{ value: 'STUNTING', label: 'Stunting' },
|
||||
]}
|
||||
value={statusFilter}
|
||||
onChange={(v) => {
|
||||
setStatusFilter(v ?? '');
|
||||
state.findMany.load(1, 10, search, v ?? '');
|
||||
}}
|
||||
radius="md"
|
||||
clearable
|
||||
/>
|
||||
<Button onClick={handleSearch} radius="md" variant="light">Cari</Button>
|
||||
</Group>
|
||||
<Group mb="md">
|
||||
<Select
|
||||
placeholder="Filter stunting"
|
||||
data={[
|
||||
{ value: '', label: 'Semua' },
|
||||
{ value: 'NORMAL', label: 'Normal' },
|
||||
{ value: 'ALERT', label: 'Alert' },
|
||||
{ value: 'STUNTING', label: 'Stunting' },
|
||||
]}
|
||||
value={statusFilter}
|
||||
onChange={(v) => setStatusFilter(v ?? '')}
|
||||
radius="md"
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{state.findMany.loading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>JK</Table.Th>
|
||||
<Table.Th>Tgl Lahir</Table.Th>
|
||||
<Table.Th>Imunisasi</Table.Th>
|
||||
<Table.Th>Gizi</Table.Th>
|
||||
<Table.Th>Pemeriksaan</Table.Th>
|
||||
<Table.Th>Stunting</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows && rows.length > 0 ? rows : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={8}>
|
||||
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="22%">Nama</TableTh>
|
||||
<TableTh w="7%">JK</TableTh>
|
||||
<TableTh w="12%">Tgl Lahir</TableTh>
|
||||
<TableTh w="12%">Imunisasi</TableTh>
|
||||
<TableTh w="10%">Gizi</TableTh>
|
||||
<TableTh w="12%">Pemeriksaan</TableTh>
|
||||
<TableTh w="11%">Stunting</TableTh>
|
||||
<TableTh w="14%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((d) => (
|
||||
<TableTr key={d.id}>
|
||||
<TableTd>{d.nama}</TableTd>
|
||||
<TableTd>{d.jenisKelamin}</TableTd>
|
||||
<TableTd>
|
||||
{d.tanggalLahir
|
||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={d.imunisasiLengkap ? 'green' : 'red'} variant="light">
|
||||
{d.imunisasiLengkap ? 'Lengkap' : 'Belum'}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={d.giziBaik ? 'green' : 'orange'} variant="light">
|
||||
{d.giziBaik ? 'Baik' : 'Kurang'}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Badge color={d.pemeriksaanRutin ? 'green' : 'orange'} variant="light">
|
||||
{d.pemeriksaanRutin ? 'Rutin' : 'Tidak'}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Badge
|
||||
color={STUNTING_COLORS[d.statusStunting] ?? 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{d.statusStunting}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/kesehatan/posyandu/balita/edit/${d.id}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={8}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data balita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{(state.findMany.totalPages ?? 1) > 1 && (
|
||||
<Group justify="center">
|
||||
<Pagination
|
||||
total={state.findMany.totalPages}
|
||||
value={state.findMany.page}
|
||||
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
|
||||
/>
|
||||
</Group>
|
||||
{/* Mobile Cards */}
|
||||
<Stack gap="sm" hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((d) => (
|
||||
<Paper key={d.id} withBorder p="md" radius="md">
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} mb={4}>
|
||||
{d.nama}
|
||||
</Text>
|
||||
<Group gap="xs" mb={6}>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.jenisKelamin}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
·
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.tanggalLahir
|
||||
? new Date(d.tanggalLahir).toLocaleDateString('id-ID')
|
||||
: '-'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs" mb={8}>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={d.imunisasiLengkap ? 'green' : 'red'}
|
||||
variant="light"
|
||||
>
|
||||
{d.imunisasiLengkap ? 'Imunisasi Lengkap' : 'Imunisasi Belum'}
|
||||
</Badge>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={d.giziBaik ? 'green' : 'orange'}
|
||||
variant="light"
|
||||
>
|
||||
Gizi {d.giziBaik ? 'Baik' : 'Kurang'}
|
||||
</Badge>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={STUNTING_COLORS[d.statusStunting] ?? 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{d.statusStunting}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/kesehatan/posyandu/balita/edit/${d.id}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data balita yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search, statusFilter);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="lg"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default BalitaPage;
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client';
|
||||
|
||||
import ibuHamilState from '@/app/admin/(dashboard)/_state/kesehatan/ibu-hamil/ibuHamil';
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
Loader,
|
||||
Pagination,
|
||||
Paper,
|
||||
Select,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTh,
|
||||
TableThead,
|
||||
TableTr,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconDeviceImacCog, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import ibuHamilState from '../../../_state/kesehatan/ibu-hamil/ibuHamil';
|
||||
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
AKTIF: 'green',
|
||||
@@ -30,142 +37,242 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
NONAKTIF: 'red',
|
||||
};
|
||||
|
||||
export default function IbuHamilPage() {
|
||||
const router = useRouter();
|
||||
const state = useProxy(ibuHamilState);
|
||||
function IbuHamilPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title="Ibu Hamil"
|
||||
placeholder="Cari nama / NIK..."
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListIbuHamil search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListIbuHamil({ search }: { search: string }) {
|
||||
const state = useProxy(ibuHamilState);
|
||||
const router = useRouter();
|
||||
const [statusFilter, setStatusFilter] = useState('');
|
||||
const [debouncedSearch] = useDebouncedValue(search, 1000);
|
||||
|
||||
const { data, page, totalPages, loading, load } = state.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
state.findMany.load(1, 10, search, statusFilter);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSearch = () => {
|
||||
state.findMany.load(1, 10, search, statusFilter);
|
||||
};
|
||||
load(page, 10, debouncedSearch, statusFilter);
|
||||
}, [page, debouncedSearch, statusFilter]);
|
||||
|
||||
const handleDelete = async (id: string, nama: string) => {
|
||||
if (!confirm(`Hapus data ibu hamil "${nama}"?`)) return;
|
||||
await state.delete.byId(id);
|
||||
};
|
||||
|
||||
const rows = state.findMany.data?.map((d) => (
|
||||
<Table.Tr key={d.id}>
|
||||
<Table.Td>{d.nama}</Table.Td>
|
||||
<Table.Td>{d.nik || '-'}</Table.Td>
|
||||
<Table.Td>{d.usiaKehamilan} minggu</Table.Td>
|
||||
<Table.Td>{d.noHp || '-'}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={STATUS_COLORS[d.status] ?? 'gray'} variant="light">
|
||||
{d.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => router.push(`/admin/kesehatan/posyandu/ibu-hamil/edit/${d.id}`)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
));
|
||||
const filteredData = data || [];
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="md">
|
||||
<Skeleton h={600} radius="md" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px={{ base: 0, md: 'lg' }} py="xs">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={3} c="black">Ibu Hamil</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={16} />}
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil/create')}
|
||||
radius="md"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Group>
|
||||
<Box py="md">
|
||||
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Title order={4}>List Ibu Hamil</Title>
|
||||
<Button
|
||||
leftSection={<IconPlus size={18} />}
|
||||
color="blue"
|
||||
variant="light"
|
||||
onClick={() => router.push('/admin/kesehatan/posyandu/ibu-hamil/create')}
|
||||
>
|
||||
Tambah Baru
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group mb="md" gap="sm">
|
||||
<TextInput
|
||||
placeholder="Cari nama / NIK..."
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
radius="md"
|
||||
style={{ flex: 1, maxWidth: 300 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter status"
|
||||
data={[
|
||||
{ value: '', label: 'Semua Status' },
|
||||
{ value: 'AKTIF', label: 'Aktif' },
|
||||
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
||||
{ value: 'KEGUGURAN', label: 'Keguguran' },
|
||||
{ value: 'NONAKTIF', label: 'Nonaktif' },
|
||||
]}
|
||||
value={statusFilter}
|
||||
onChange={(v) => {
|
||||
setStatusFilter(v ?? '');
|
||||
state.findMany.load(1, 10, search, v ?? '');
|
||||
}}
|
||||
radius="md"
|
||||
clearable
|
||||
/>
|
||||
<Button onClick={handleSearch} radius="md" variant="light">Cari</Button>
|
||||
</Group>
|
||||
<Group mb="md">
|
||||
<Select
|
||||
placeholder="Filter status"
|
||||
data={[
|
||||
{ value: '', label: 'Semua Status' },
|
||||
{ value: 'AKTIF', label: 'Aktif' },
|
||||
{ value: 'MELAHIRKAN', label: 'Melahirkan' },
|
||||
{ value: 'KEGUGURAN', label: 'Keguguran' },
|
||||
{ value: 'NONAKTIF', label: 'Nonaktif' },
|
||||
]}
|
||||
value={statusFilter}
|
||||
onChange={(v) => setStatusFilter(v ?? '')}
|
||||
radius="md"
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{state.findMany.loading ? (
|
||||
<Group justify="center" py="xl"><Loader /></Group>
|
||||
) : (
|
||||
<Stack gap="md">
|
||||
<Table striped highlightOnHover withTableBorder>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>NIK</Table.Th>
|
||||
<Table.Th>Usia Kehamilan</Table.Th>
|
||||
<Table.Th>No. HP</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows && rows.length > 0 ? rows : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={6}>
|
||||
<Text c="dimmed" ta="center" py="md">Tidak ada data</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{/* Desktop Table */}
|
||||
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
|
||||
<Table highlightOnHover layout="fixed" withColumnBorders={false}>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="25%">Nama</TableTh>
|
||||
<TableTh w="18%">NIK</TableTh>
|
||||
<TableTh w="17%">Usia Kehamilan</TableTh>
|
||||
<TableTh w="15%">No. HP</TableTh>
|
||||
<TableTh w="12%">Status</TableTh>
|
||||
<TableTh w="13%">Aksi</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((d) => (
|
||||
<TableTr key={d.id}>
|
||||
<TableTd>{d.nama}</TableTd>
|
||||
<TableTd>{d.nik || '-'}</TableTd>
|
||||
<TableTd>{d.usiaKehamilan} minggu</TableTd>
|
||||
<TableTd>{d.noHp || '-'}</TableTd>
|
||||
<TableTd>
|
||||
<Badge
|
||||
color={STATUS_COLORS[d.status] ?? 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{d.status}
|
||||
</Badge>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/kesehatan/posyandu/ibu-hamil/edit/${d.id}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))
|
||||
) : (
|
||||
<TableTr>
|
||||
<TableTd colSpan={6}>
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data ibu hamil yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{(state.findMany.totalPages ?? 1) > 1 && (
|
||||
<Group justify="center">
|
||||
<Pagination
|
||||
total={state.findMany.totalPages}
|
||||
value={state.findMany.page}
|
||||
onChange={(p) => state.findMany.load(p, 10, search, statusFilter)}
|
||||
/>
|
||||
</Group>
|
||||
{/* Mobile Cards */}
|
||||
<Stack gap="sm" hiddenFrom="md">
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((d) => (
|
||||
<Paper key={d.id} withBorder p="md" radius="md">
|
||||
<Box>
|
||||
<Text fz="sm" fw={600} mb={4}>
|
||||
{d.nama}
|
||||
</Text>
|
||||
<Group gap="xs" mb={6}>
|
||||
<Text fz="xs" c="dimmed">
|
||||
NIK: {d.nik || '-'}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
·
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.usiaKehamilan} minggu
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="xs" mb={8}>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={STATUS_COLORS[d.status] ?? 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{d.status}
|
||||
</Badge>
|
||||
{d.noHp && (
|
||||
<Text fz="xs" c="dimmed">
|
||||
{d.noHp}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="blue"
|
||||
leftSection={<IconDeviceImacCog size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/admin/kesehatan/posyandu/ibu-hamil/edit/${d.id}`)
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="red"
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={() => handleDelete(d.id, d.nama)}
|
||||
loading={state.delete.loading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
</Paper>
|
||||
))
|
||||
) : (
|
||||
<Center py="xl">
|
||||
<Text c="dimmed" fz="sm" lh={1.4}>
|
||||
Tidak ada data ibu hamil yang cocok
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10, search, statusFilter);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="lg"
|
||||
mb="md"
|
||||
color="blue"
|
||||
radius="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default IbuHamilPage;
|
||||
|
||||
@@ -37,7 +37,7 @@ import { getNavbar } from "./(dashboard)/user&role/_com/dynamicNavbar";
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { isDark } = useDarkMode();
|
||||
const tokens = themeTokens(isDark);
|
||||
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -114,7 +114,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
case 2:
|
||||
return '/admin/landing-page/profil/program-inovasi';
|
||||
case 3:
|
||||
return '/admin/kesehatan/posyandu';
|
||||
return '/admin/kesehatan/posyandu/list-posyandu';
|
||||
case 4:
|
||||
return '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
default:
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function WaitingRoom() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
|
||||
// ⏱️ Countdown timer
|
||||
const [timeLeft, setTimeLeft] = useState(CONFIG.TIMEOUT_DURATION / 1000); // dalam detik
|
||||
const [hasTimedOut, setHasTimedOut] = useState(false);
|
||||
@@ -128,7 +128,7 @@ export default function WaitingRoom() {
|
||||
redirectPath = '/admin/landing-page/profil/program-inovasi';
|
||||
break;
|
||||
case "3":
|
||||
redirectPath = '/admin/kesehatan/posyandu';
|
||||
redirectPath = '/admin/kesehatan/posyandu/list-posyandu';
|
||||
break;
|
||||
case "4":
|
||||
redirectPath = '/admin/pendidikan/info-sekolah/jenjang-pendidikan';
|
||||
@@ -200,9 +200,9 @@ export default function WaitingRoom() {
|
||||
Silakan hubungi Superadmin atau coba login ulang nanti.
|
||||
</Text>
|
||||
<Group gap="sm" w="100%">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
Kembali ke Login
|
||||
@@ -243,11 +243,11 @@ export default function WaitingRoom() {
|
||||
<Title order={2} c={colors['blue-button']} ta="center">
|
||||
⏳ Menunggu Persetujuan
|
||||
</Title>
|
||||
|
||||
|
||||
<Text ta="center" c="dimmed">
|
||||
Akun Anda sedang dalam proses verifikasi oleh Superadmin.
|
||||
</Text>
|
||||
|
||||
|
||||
<Text ta="center" size="sm" fw={500}>
|
||||
Nomor: {user?.nomor || '...'}
|
||||
</Text>
|
||||
@@ -260,8 +260,8 @@ export default function WaitingRoom() {
|
||||
{formatTime(timeLeft)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={progressValue}
|
||||
<Progress
|
||||
value={progressValue}
|
||||
color={timeLeft < 60 ? 'red' : colors['blue-button']}
|
||||
size="sm"
|
||||
animated
|
||||
@@ -269,15 +269,15 @@ export default function WaitingRoom() {
|
||||
</Stack>
|
||||
|
||||
<Loader size="sm" color={colors['blue-button']} />
|
||||
|
||||
|
||||
<Text ta="center" size="xs" c="dimmed">
|
||||
Jangan tutup halaman ini. Anda akan dialihkan otomatis setelah disetujui.
|
||||
</Text>
|
||||
|
||||
{/* 🚪 Tombol Keluar */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={handleLogout}
|
||||
c="dimmed"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user