feat: add kependudukan seeders, API routes, year filter, and navbar menu

- Add Prisma models: DataBanjar, DistribusiAgama, DistribusiUmur, MigrasiPenduduk, DinamikaPenduduk
- Create seeders for all kependudukan models with year 2026 data
- Register Kependudukan API routes in route.ts
- Update API findMany endpoints to make tahun parameter optional
- Add YearFilter reusable component for admin pages
- Update 4 kependudukan admin pages with year filter UI
- Fix Mantine color array in AdminThemeProvider (add 10th element)
- Fix invalid Mantine color scale in paguTable.tsx (gray.50 -> gray.1)
- Add Kependudukan menu to navbar-list-menu.ts
- Fix Bun JSON import resolution with loadJsonData helper
- Update 74 seeder files to use dynamic JSON loading

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-10 11:54:36 +08:00
parent 5e822f0b05
commit 8b14c6ce44
146 changed files with 3051 additions and 201 deletions

View File

@@ -58,14 +58,17 @@ const dataBanjar = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
tahun: undefined as number | undefined,
load: async (page = 1, limit = 10, search = "", tahun?: number) => {
dataBanjar.findMany.loading = true;
dataBanjar.findMany.page = page;
dataBanjar.findMany.search = search;
dataBanjar.findMany.tahun = tahun;
try {
const query: any = { page, limit, tahun };
const query: any = { page, limit };
if (search) query.search = search;
if (tahun) query.tahun = tahun;
const res = await ApiFetch.api.kependudukan.databanjar["find-many"].get({ query });

View File

@@ -54,13 +54,14 @@ const distribusiAgama = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
load: async (page = 1, limit = 10, search = "", tahun?: number) => {
distribusiAgama.findMany.loading = true;
distribusiAgama.findMany.page = page;
distribusiAgama.findMany.search = search;
try {
const query: any = { page, limit, tahun };
const query: any = { page, limit };
if (tahun) query.tahun = tahun;
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.distribusiagama["find-many"].get({ query });

View File

@@ -54,13 +54,14 @@ const distribusiUmur = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
load: async (page = 1, limit = 10, search = "", tahun?: number) => {
distribusiUmur.findMany.loading = true;
distribusiUmur.findMany.page = page;
distribusiUmur.findMany.search = search;
try {
const query: any = { page, limit, tahun };
const query: any = { page, limit };
if (tahun) query.tahun = tahun;
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.distribusiumur["find-many"].get({ query });

View File

@@ -60,13 +60,14 @@ const migrasiPenduduk = proxy({
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
load: async (page = 1, limit = 10, search = "", tahun?: number) => {
migrasiPenduduk.findMany.loading = true;
migrasiPenduduk.findMany.page = page;
migrasiPenduduk.findMany.search = search;
try {
const query: any = { page, limit, tahun };
const query: any = { page, limit };
if (tahun) query.tahun = tahun;
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.migrasipenduduk["find-many"].get({ query });

View File

@@ -0,0 +1,43 @@
import { Select, Group } from '@mantine/core';
import { IconCalendar } from '@tabler/icons-react';
interface YearFilterProps {
value?: number;
onChange: (year: number | undefined) => void;
minYear?: number;
maxYear?: number;
}
export function YearFilter({
value,
onChange,
minYear = 2020,
maxYear = new Date().getFullYear() + 1,
}: YearFilterProps) {
const years = Array.from(
{ length: maxYear - minYear + 1 },
(_, i) => maxYear - i
);
const options = [
{ value: '', label: 'Semua Tahun' },
...years.map((year) => ({
value: year.toString(),
label: year.toString(),
})),
];
return (
<Select
placeholder="Pilih Tahun"
data={options}
value={value?.toString() ?? ''}
onChange={(val) => onChange(val ? parseInt(val) : undefined)}
leftSection={<IconCalendar size={18} />}
clearable
w={200}
/>
);
}
export default YearFilter;

View File

@@ -27,9 +27,12 @@ import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import dataBanjar from '../../_state/kependudukan/data-banjar';
import { YearFilter } from '../_components/YearFilter';
function DataBanjarAdmin() {
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<number | undefined>(undefined);
return (
<Box>
<HeaderSearch
@@ -39,12 +42,18 @@ function DataBanjarAdmin() {
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDataBanjar search={search} />
<Box mt="md">
<YearFilter value={selectedYear} onChange={(year) => {
setSelectedYear(year);
dataBanjar.findMany.page = 1;
}} />
</Box>
<ListDataBanjar search={search} year={selectedYear} />
</Box>
);
}
function ListDataBanjar({ search }: { search: string }) {
function ListDataBanjar({ search, year }: { search: string; year?: number }) {
type DataBanjarType = {
id: string;
nama: string;
@@ -77,8 +86,8 @@ function ListDataBanjar({ search }: { search: string }) {
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
load(page, 10, debouncedSearch, year);
}, [page, debouncedSearch, year]);
const filteredData = data || [];

View File

@@ -25,12 +25,14 @@ import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { YearFilter } from '../_components/YearFilter';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import distribusiAgama from '../../_state/kependudukan/distribusi-agama';
function DistribusiAgamaAdmin() {
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<number | undefined>(undefined);
return (
<Box>
<HeaderSearch
@@ -40,12 +42,18 @@ function DistribusiAgamaAdmin() {
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDistribusiAgama search={search} />
<Box mt="md">
<YearFilter value={selectedYear} onChange={(year) => {
setSelectedYear(year);
distribusiAgama.findMany.page = 1;
}} />
</Box>
<ListDistribusiAgama search={search} year={selectedYear} />
</Box>
);
}
function ListDistribusiAgama({ search }: { search: string }) {
function ListDistribusiAgama({ search, year }: { search: string; year?: number }) {
type DistribusiAgamaType = {
id: string;
agama: string;
@@ -76,8 +84,8 @@ function ListDistribusiAgama({ search }: { search: string }) {
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
load(page, 10, debouncedSearch, year);
}, [page, debouncedSearch, year]);
const filteredData = data || [];

View File

@@ -25,12 +25,14 @@ import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { YearFilter } from '../_components/YearFilter';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import distribusiUmur from '../../_state/kependudukan/distribusi-umur';
function DistribusiUmurAdmin() {
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<number | undefined>(undefined);
return (
<Box>
<HeaderSearch
@@ -40,12 +42,18 @@ function DistribusiUmurAdmin() {
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDistribusiUmur search={search} />
<Box mt="md">
<YearFilter value={selectedYear} onChange={(year) => {
setSelectedYear(year);
distribusiUmur.findMany.page = 1;
}} />
</Box>
<ListDistribusiUmur search={search} year={selectedYear} />
</Box>
);
}
function ListDistribusiUmur({ search }: { search: string }) {
function ListDistribusiUmur({ search, year }: { search: string; year?: number }) {
type DistribusiUmurType = {
id: string;
rentangUmur: string;
@@ -77,8 +85,8 @@ function ListDistribusiUmur({ search }: { search: string }) {
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
load(page, 10, debouncedSearch, year);
}, [page, debouncedSearch, year]);
const filteredData = data || [];

View File

@@ -24,12 +24,14 @@ import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { YearFilter } from '../_components/YearFilter';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import migrasiPenduduk from '../../_state/kependudukan/migrasi-penduduk';
function MigrasiPendudukAdmin() {
const [search, setSearch] = useState('');
const [selectedYear, setSelectedYear] = useState<number | undefined>(undefined);
return (
<Box>
<HeaderSearch
@@ -39,12 +41,18 @@ function MigrasiPendudukAdmin() {
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListMigrasiPenduduk search={search} />
<Box mt="md">
<YearFilter value={selectedYear} onChange={(year) => {
setSelectedYear(year);
migrasiPenduduk.findMany.page = 1;
}} />
</Box>
<ListMigrasiPenduduk search={search} year={selectedYear} />
</Box>
);
}
function ListMigrasiPenduduk({ search }: { search: string }) {
function ListMigrasiPenduduk({ search, year }: { search: string; year?: number }) {
type MigrasiPendudukType = {
id: string;
jenis: string;
@@ -78,8 +86,8 @@ function ListMigrasiPenduduk({ search }: { search: string }) {
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
load(page, 10, debouncedSearch, year);
}, [page, debouncedSearch, year]);
const filteredData = data || [];