Add Layanan Polsek submenu polsek terdekat

Seeder menu keamanan -> menu ekonomi submenu : demografi pekerjaan, junlah pengangguran, lowongan kerja lokal, pasar desa, program kemiskinan, sektor unggulan, struktur organisasi
This commit is contained in:
2026-01-17 10:32:48 +08:00
parent 184854d273
commit 17b20e0d40
67 changed files with 3238 additions and 1079 deletions

View File

@@ -0,0 +1,279 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import statePolsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat";
import colors from "@/con/colors";
import {
Box,
Button,
Group,
Loader,
MultiSelect,
Paper,
Stack,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
type FormData = {
nama: string;
jarakKeDesa: string;
alamat: string;
nomorTelepon: string;
jamOperasional: string;
embedMapUrl: string;
namaTempatMaps: string;
alamatMaps: string;
linkPetunjukArah: string;
layananPolsekId: string[];
};
function EditPolsekTerdekat() {
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const params = useParams();
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: []
});
const [originalData, setOriginalData] = useState({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: []
});
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
// load data untuk form edit
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
setOriginalData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const handleChange = (key: keyof FormData, value: any) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
jarakKeDesa: originalData.jarakKeDesa,
alamat: originalData.alamat,
nomorTelepon: originalData.nomorTelepon,
jamOperasional: originalData.jamOperasional,
embedMapUrl: originalData.embedMapUrl,
namaTempatMaps: originalData.namaTempatMaps,
alamatMaps: originalData.alamatMaps,
linkPetunjukArah: originalData.linkPetunjukArah,
layananPolsekId: (originalData as any)?.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await polsekState.edit.update();
toast.success("Polsek terdekat berhasil diperbarui!");
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memperbarui data polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Polsek Terdekat
</Title>
</Group>
{/* Form utama */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Input fields */}
<TextInput
value={formData.nama}
onChange={(e) => handleChange("nama", e.currentTarget.value)}
label="Nama Polsek Terdekat"
placeholder="Masukkan nama Polsek Terdekat"
required
/>
<TextInput
value={formData.jarakKeDesa}
onChange={(e) => handleChange("jarakKeDesa", e.currentTarget.value)}
label="Jarak Polsek Terdekat"
/>
<TextInput
value={formData.alamat}
onChange={(e) => handleChange("alamat", e.currentTarget.value)}
label="Alamat Polsek Terdekat"
/>
<TextInput
value={formData.nomorTelepon}
onChange={(e) => handleChange("nomorTelepon", e.currentTarget.value)}
label="Nomor Telepon"
/>
<TextInput
value={formData.jamOperasional}
onChange={(e) => handleChange("jamOperasional", e.currentTarget.value)}
label="Jam Operasional"
/>
<TextInput
value={formData.embedMapUrl}
onChange={(e) => handleChange("embedMapUrl", e.currentTarget.value)}
label="Embed Map URL"
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(e) => handleChange("namaTempatMaps", e.currentTarget.value)}
label="Nama Tempat Maps"
/>
<TextInput
value={formData.alamatMaps}
onChange={(e) => handleChange("alamatMaps", e.currentTarget.value)}
label="Alamat Maps"
/>
<TextInput
value={formData.linkPetunjukArah}
onChange={(e) => handleChange("linkPetunjukArah", e.currentTarget.value)}
label="Link Petunjuk Arah"
/>
<MultiSelect
label="Layanan Polsekl"
placeholder="Pilih layanan polsek"
value={formData.layananPolsekId}
onChange={(val) => handleChange('layananPolsekId', val)}
data={
statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
}
clearable
searchable
required
error={!formData.layananPolsekId.length ? 'Pilih minimal satu layanan polsek' : undefined}
/>
{/* Submit */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPolsekTerdekat;

View File

@@ -0,0 +1,209 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../../_com/modalKonfirmasiHapus';
import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat';
function DetailPolsekTerdekat() {
const router = useRouter();
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false);
const params = useParams();
useShallowEffect(() => {
polsekState.findUnique.load(params?.id as string);
}, []);
const handleHapus = () => {
if (selectedId) {
polsekState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
}
};
if (!polsekState.findUnique.data) {
return (
<Stack py={10}>
<Skeleton height={500} radius="md" />
</Stack>
);
}
const data = polsekState.findUnique.data;
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* 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: "70%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Polsek Terdekat
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
{/* Nama */}
<Box>
<Text fz="lg" fw="bold">Nama Polsek Terdekat</Text>
<Text fz="md" c="dimmed">{data?.nama || "-"}</Text>
</Box>
{/* Jarak */}
<Box>
<Text fz="lg" fw="bold">Jarak Polsek ke Desa</Text>
<Text fz="md" c="dimmed">{data?.jarakKeDesa || "-"}</Text>
</Box>
{/* Alamat */}
<Box>
<Text fz="lg" fw="bold">Alamat Polsek</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data?.alamat || "-"}</Text>
</Box>
{/* Nomor */}
<Box>
<Text fz="lg" fw="bold">Nomor Polsek</Text>
<Text fz="md" c="dimmed">{data?.nomorTelepon || "-"}</Text>
</Box>
{/* Jam Operasional */}
<Box>
<Text fz="lg" fw="bold">Jam Operasional</Text>
<Text fz="md" c="dimmed">{data?.jamOperasional || "-"}</Text>
</Box>
{/* Google Maps */}
<Box>
<Text fz="lg" fw="bold">Google Maps</Text>
{data?.embedMapUrl ? (
<Box style={{ position: 'relative', paddingBottom: '56.25%', height: 0, overflow: 'hidden' }}>
<iframe
src={data.embedMapUrl}
width="100%"
height="100%"
style={{
border: 0,
position: 'absolute',
top: 0,
left: 0,
}}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
/>
</Box>
) : (
<Text c="dimmed" fz="sm">Tidak ada maps</Text>
)}
</Box>
{/* Nama Tempat Maps */}
<Box>
<Text fz="lg" fw="bold">Nama Tempat Maps</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data?.namaTempatMaps || "-"}</Text>
</Box>
{/* Alamat Maps */}
<Box>
<Text fz="lg" fw="bold">Alamat Maps</Text>
<Text fz="md" c="dimmed" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>{data?.alamatMaps || "-"}</Text>
</Box>
{/* Link Petunjuk Arah */}
<Box>
<Text fz="lg" fw="bold" >Link Petunjuk Arah</Text>
<Text fz="md" style={{ wordBreak: "break-word", whiteSpace: "normal" }}>
<a
href={data?.linkPetunjukArah || "#"}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'black', textDecoration: 'underline' }}
>
{data?.linkPetunjukArah || "Tidak ada link"}
</a>
</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Layanan Polsek</Text>
<Stack gap={4}>
{data.LayananToPolsek && data.LayananToPolsek.length > 0 ? (
data.LayananToPolsek.map((layanan) => (
<Text fz="md" c="dimmed" key={layanan.id}>
{layanan.layanan.nama}
</Text>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada layanan polsek</Text>
)}
</Stack>
</Box>
{/* Aksi */}
<Group gap="sm">
<Button
color="red"
onClick={() => {
setSelectedId(data.id);
setModalHapus(true);
}}
variant="light"
radius="md"
size="md"
>
<IconTrash size={20} />
</Button>
<Button
color="green"
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${data.id}/edit`)}
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} />
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus polsek terdekat ini?"
/>
</Box>
);
}
export default DetailPolsekTerdekat;

View File

@@ -0,0 +1,237 @@
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
MultiSelect,
Paper,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat';
function CreatePolsekTerdekat() {
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
const resetForm = () => {
polsekState.create.form = {
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: [],
};
};
const isValidGoogleMapsEmbed = (url: string): boolean => {
try {
const u = new URL(url);
return (
u.hostname === 'www.google.com' &&
u.pathname === '/maps/embed' &&
u.searchParams.has('pb')
);
} catch {
return false;
}
};
const handleSubmit = async () => {
const { embedMapUrl } = polsekState.create.form;
// ✅ Validasi Google Maps Embed URL (jika diisi)
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
return;
}
try {
setIsSubmitting(true);
await polsekState.create.create();
resetForm();
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
} catch (error) {
console.error(error);
toast.error("Gagal menambah polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
const extractEmbedUrl = (input: string): string => {
// Jika sudah berupa URL embed yang valid
if (input.startsWith('https://www.google.com/maps/embed?')) {
return input.trim();
}
// Coba parse sebagai HTML string (iframe)
const iframeRegex = /<iframe[^>]*src=["']([^"']*)["'][^>]*>/i;
const match = input.match(iframeRegex);
if (match && match[1]?.startsWith('https://www.google.com/maps/embed?')) {
return match[1].trim();
}
// Jika tidak cocok, kembalikan input asli (atau string kosong)
return input.trim();
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Polsek Terdekat
</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={polsekState.create.form.nama}
onChange={(val) => (polsekState.create.form.nama = val.target.value)}
label={<Text fw="bold" fz="sm">Nama Polsek Terdekat</Text>}
placeholder="Masukkan nama Polsek Terdekat"
required
/>
<TextInput
value={polsekState.create.form.jarakKeDesa}
onChange={(val) => (polsekState.create.form.jarakKeDesa = val.target.value)}
label={<Text fw="bold" fz="sm">Jarak Polsek Terdekat</Text>}
placeholder="Masukkan jarak Polsek Terdekat"
required
/>
<TextInput
value={polsekState.create.form.alamat}
onChange={(val) => (polsekState.create.form.alamat = val.target.value)}
label={<Text fw="bold" fz="sm">Alamat Polsek Terdekat</Text>}
placeholder="Masukkan alamat Polsek Terdekat"
required
/>
<TextInput
value={polsekState.create.form.nomorTelepon}
onChange={(val) => (polsekState.create.form.nomorTelepon = val.target.value)}
label={<Text fw="bold" fz="sm">Nomor Telepon Polsek Terdekat</Text>}
placeholder="Masukkan nomor telepon Polsek Terdekat"
required
/>
<TextInput
value={polsekState.create.form.jamOperasional}
onChange={(val) => (polsekState.create.form.jamOperasional = val.target.value)}
label={<Text fw="bold" fz="sm">Jam Operasional Polsek Terdekat</Text>}
placeholder="Masukkan jam operasional Polsek Terdekat"
/>
<TextInput
value={polsekState.create.form.embedMapUrl}
onChange={(e) => {
const rawValue = e.currentTarget.value;
const cleanUrl = extractEmbedUrl(rawValue);
polsekState.create.form.embedMapUrl = cleanUrl;
}}
description="Contoh: https://www.google.com/maps/embed?pb=..."
label={<Text fw="bold" fz="sm">Embed Map URL</Text>}
placeholder="Paste iframe dari Google Maps atau URL embed langsung"
/>
<TextInput
value={polsekState.create.form.namaTempatMaps}
onChange={(val) => (polsekState.create.form.namaTempatMaps = val.target.value)}
label={<Text fw="bold" fz="sm">Nama Tempat Maps</Text>}
placeholder="Masukkan nama tempat maps"
/>
<TextInput
value={polsekState.create.form.alamatMaps}
onChange={(val) => (polsekState.create.form.alamatMaps = val.target.value)}
label={<Text fw="bold" fz="sm">Alamat Maps</Text>}
placeholder="Masukkan alamat maps"
/>
<TextInput
value={polsekState.create.form.linkPetunjukArah}
onChange={(val) => (polsekState.create.form.linkPetunjukArah = val.target.value)}
label={<Text fw="bold" fz="sm">Link Petunjuk Arah</Text>}
placeholder="Masukkan link petunjuk arah"
/>
<MultiSelect
label="Layanan Polsek"
placeholder="Pilih layanan polsek (bisa lebih dari satu)"
data={statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []}
value={polsekState.create.form.layananPolsekId}
onChange={(val) => {
polsekState.create.form.layananPolsekId = val;
}}
searchable
clearable
nothingFoundMessage="Tidak ada layanan ditemukan"
required
error={polsekState.create.form.layananPolsekId?.length === 0 ? "Pilih minimal 1 layanan polsek" : undefined}
/>
{/* Tombol Submit */}
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<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)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreatePolsekTerdekat;

View File

@@ -0,0 +1,252 @@
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import statePolsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
function PolsekTerdekat() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Polsek Terdekat'
placeholder='Cari nama atau alamat...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListPolsekTerdekat search={search} />
</Box>
);
}
function ListPolsekTerdekat({ search }: { search: string }) {
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = polsekState.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={{ base: 'sm', md: 'lg' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={4} lh={{ base: 1.2, md: 1.2 }}>
Daftar Polsek Terdekat
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Nama Polsek
</Text>
</TableTh>
<TableTh style={{ width: '15%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Jarak
</Text>
</TableTh>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Alamat
</Text>
</TableTh>
<TableTh style={{ width: '10%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Aksi
</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.nama}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5}>
{item.jarakKeDesa}
</Text>
</TableTd>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.alamat}
</Text>
</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${item.id}`)}
w="100%"
>
<IconDeviceImac size={18} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data Polsek yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Polsek
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jarak
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.jarakKeDesa}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Alamat
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.alamat}
</Text>
</Box>
<Box>
<Button
variant="light"
color="blue"
fullWidth
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${item.id}`)}
>
<IconDeviceImac size={18} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>
Detail
</Text>
</Button>
</Box>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data Polsek yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt={{ base: 'lg', md: 'xl' }}
mb={{ base: 'lg', md: 'xl' }}
color="blue"
radius="md"
/>
</Center>
</Box>
);
}
export default PolsekTerdekat;