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:
2026-05-05 16:12:46 +08:00
parent ff25ead2df
commit e71c938b2f
6 changed files with 873 additions and 620 deletions

View File

@@ -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:

View File

@@ -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;

View File

@@ -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;

View File

@@ -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:

View File

@@ -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"
>