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 || [];

View File

@@ -6,10 +6,15 @@ export default async function dataBanjarFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 100;
const search = (context.query.search as string) || '';
const tahun = Number(context.query.tahun) || new Date().getFullYear();
const tahun = context.query.tahun ? Number(context.query.tahun) : undefined;
const skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
const where: any = { isActive: true };
// Filter by tahun hanya jika dikirim
if (tahun) {
where.tahun = tahun;
}
if (search) {
where.OR = [

View File

@@ -6,10 +6,15 @@ export default async function distribusiAgamaFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 100;
const search = (context.query.search as string) || '';
const tahun = Number(context.query.tahun) || new Date().getFullYear();
const tahun = context.query.tahun ? Number(context.query.tahun) : undefined;
const skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
const where: any = { isActive: true };
// Filter by tahun hanya jika dikirim
if (tahun) {
where.tahun = tahun;
}
// Tambahkan pencarian (jika ada)
if (search) {

View File

@@ -5,10 +5,15 @@ import { Context } from "elysia";
export default async function distribusiUmurFindMany(context: Context) {
const page = Number(context.query.page) || 1;
const limit = Number(context.query.limit) || 100;
const tahun = Number(context.query.tahun) || new Date().getFullYear();
const tahun = context.query.tahun ? Number(context.query.tahun) : undefined;
const skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
const where: any = { isActive: true };
// Filter by tahun hanya jika dikirim
if (tahun) {
where.tahun = tahun;
}
try {
const [data, total] = await Promise.all([

View File

@@ -7,7 +7,7 @@ export default async function migrasiPendudukFindMany(context: Context) {
const limit = Number(context.query.limit) || 10;
const search = (context.query.search as string) || '';
const jenis = (context.query.jenis as string) || '';
const tahun = Number(context.query.tahun) || new Date().getFullYear();
const tahun = context.query.tahun ? Number(context.query.tahun) : undefined;
const skip = (page - 1) * limit;
const where: any = { isActive: true };
@@ -16,6 +16,7 @@ export default async function migrasiPendudukFindMany(context: Context) {
where.jenis = jenis;
}
// Filter by tahun hanya jika dikirim
if (tahun) {
where.tanggal = {
gte: new Date(`${tahun}-01-01`),

View File

@@ -23,6 +23,7 @@ import Inovasi from "./_lib/inovasi";
import Lingkungan from "./_lib/lingkungan";
import LandingPage from "./_lib/landing_page";
import Pendidikan from "./_lib/pendidikan";
import Kependudukan from "./_lib/kependudukan";
import User from "./_lib/user";
import Role from "./_lib/user/role";
import Search from "./_lib/search";
@@ -119,6 +120,7 @@ const ApiServer = new Elysia({ prefix: "/api" })
.use(Inovasi)
.use(Lingkungan)
.use(Pendidikan)
.use(Kependudukan)
.use(User)
.use(Role)
.use(Search)

View File

@@ -21,10 +21,10 @@ function Section({ title, data, badgeColor = 'blue' }: SectionProps) {
</Table.Tr>
{data.map((item, index) => (
<Table.Tr
key={item.id}
bg={index % 2 === 1 ? 'gray.50' : 'white'}
style={{
<Table.Tr
key={item.id}
bg={index % 2 === 1 ? 'gray.1' : 'white'}
style={{
transition: 'background-color 0.2s ease',
':hover': {
backgroundColor: 'var(--mantine-color-blue-0)',

View File

@@ -46,6 +46,7 @@ export function AdminThemeProvider({ children, forceTheme }: AdminThemeProviderP
tokens.colors.primaryDark,
tokens.colors.primaryDark,
tokens.colors.primaryDark,
tokens.colors.primaryDark,
],
},
primaryColor: 'primary',

View File

@@ -295,51 +295,82 @@ const navbarListMenu = [
},
{
id: "8",
name: "Pendidikan",
name: "Kependudukan",
children: [
{
id: "8.1",
name: "Dashboard Kependudukan",
href: "/darmasaba/kependudukan/dashboard"
},
{
id: "8.2",
name: "Data Per Banjar",
href: "/darmasaba/kependudukan/data-per-banjar"
},
{
id: "8.3",
name: "Dinamika Penduduk",
href: "/darmasaba/kependudukan/dinamika-penduduk"
},
{
id: "8.4",
name: "Distribusi Agama",
href: "/darmasaba/kependudukan/distribusi-agama"
},
{
id: "8.5",
name: "Distribusi Umur",
href: "/darmasaba/kependudukan/distribusi-umur"
}
]
},
{
id: "9",
name: "Pendidikan",
children: [
{
id: "9.1",
name: "Info Sekolah",
href: "/darmasaba/pendidikan/info-sekolah/semua"
},
{
id: "8.2",
id: "9.2",
name: "Beasiswa Desa",
href: "/darmasaba/pendidikan/beasiswa-desa"
},
{
id: "8.3",
id: "9.3",
name: "Program Pendidikan Anak",
href: "/darmasaba/pendidikan/program-pendidikan-anak"
},
{
id: "8.4",
id: "9.4",
name: "Bimbingan Belajar Desa",
href: "/darmasaba/pendidikan/bimbingan-belajar-desa"
},
{
id: "8.5",
id: "9.5",
name: "Pendidikan Non Formal",
href: "/darmasaba/pendidikan/pendidikan-non-formal"
},
{
id: "8.6",
id: "9.6",
name: "Perpustakaan Digital",
href: "/darmasaba/pendidikan/perpustakaan-digital/semua"
},
{
id: "8.7",
id: "9.7",
name: "Data Pendidikan",
href: "/darmasaba/pendidikan/data-pendidikan"
}
]
},
{
id: "9",
id: "10",
name: "Musik",
children: [
{
id: "9.1",
id: "10.1",
name: "Musik Desa",
href: "/darmasaba/musik/musik-desa"
}