feat: implement Kependudukan menu with CRUD admin pages

- Add Distribusi Umur admin pages (list, create, edit)
- Add Data Banjar admin pages (list, create, edit)
- Add Migrasi Penduduk admin pages (list, create, edit)
- Update state management with full CRUD operations for all modules
- Add Kependudukan menu to admin sidebar (devBar, navBar, role1)
- Add public pages for Distribusi Umur with age range sorting
- Update Dinamika Penduduk to use real-time birth/death data
- Add Biome configuration for code linting
- Create API routes for all Kependudukan modules

Features:
- Pagination and search for all admin list pages
- Responsive design (table for desktop, cards for mobile)
- Delete confirmation modal
- Toast notifications for user feedback
- Zod validation for all forms
- Age range auto-sorting in public Distribusi Umur chart

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-09 17:10:29 +08:00
parent 34a37dc63b
commit 5e822f0b05
51 changed files with 5964 additions and 0 deletions

49
biome.json Normal file
View File

@@ -0,0 +1,49 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"experimentalScannerIgnores": [
"node_modules",
".next",
"out",
"public"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "warn",
"noUnusedImports": "warn"
},
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"noNonNullAssertion": "warn"
},
"complexity": {
"noForEach": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
}
}

View File

@@ -0,0 +1,27 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
const kependudukanDashboard = proxy({
summary: {
data: null as any,
loading: false,
async load() {
kependudukanDashboard.summary.loading = true;
try {
const res = await ApiFetch.api.kependudukan.dashboard.summary.get();
if (res.status === 200 && res.data?.success) {
kependudukanDashboard.summary.data = res.data.data;
} else {
kependudukanDashboard.summary.data = null;
}
} catch (err) {
console.error("Gagal fetch dashboard summary:", err);
kependudukanDashboard.summary.data = null;
} finally {
kependudukanDashboard.summary.loading = false;
}
},
},
});
export default kependudukanDashboard;

View File

@@ -0,0 +1,205 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
import { toast } from "react-toastify";
import { z } from "zod";
const templateDataBanjar = z.object({
nama: z.string().min(1, "Nama banjar harus diisi"),
penduduk: z.number().min(0, "Jumlah penduduk harus diisi"),
kk: z.number().min(0, "Jumlah KK harus diisi"),
miskin: z.number().min(0, "Jumlah penduduk miskin harus diisi"),
tahun: z.number().min(2000, "Tahun harus diisi"),
});
const dataBanjar = proxy({
create: {
form: {
nama: "",
penduduk: 0,
kk: 0,
miskin: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async create() {
const cek = templateDataBanjar.safeParse(dataBanjar.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
dataBanjar.create.loading = true;
const res = await ApiFetch.api.kependudukan.databanjar["create"].post(dataBanjar.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Sukses menambahkan data banjar");
dataBanjar.create.form = { nama: "", penduduk: 0, kk: 0, miskin: 0, tahun: new Date().getFullYear() };
dataBanjar.findMany.load();
return id;
}
}
toast.error("Gagal menambahkan data");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
dataBanjar.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
dataBanjar.findMany.loading = true;
dataBanjar.findMany.page = page;
dataBanjar.findMany.search = search;
try {
const query: any = { page, limit, tahun };
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.databanjar["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
dataBanjar.findMany.data = res.data.data ?? [];
dataBanjar.findMany.totalPages = res.data.totalPages ?? 1;
} else {
dataBanjar.findMany.data = [];
dataBanjar.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch data banjar paginated:", err);
dataBanjar.findMany.data = [];
dataBanjar.findMany.totalPages = 1;
} finally {
dataBanjar.findMany.loading = false;
}
},
},
findUnique: {
data: null as any | null,
async load(id: string) {
try {
const res = await fetch(`/api/kependudukan/databanjar/${id}`);
if (res.ok) {
const data = await res.json();
dataBanjar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data banjar:", res.statusText);
dataBanjar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data banjar:", error);
dataBanjar.findUnique.data = null;
}
},
},
update: {
id: "",
form: {
nama: "",
penduduk: 0,
kk: 0,
miskin: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
nama: this.form.nama,
penduduk: this.form.penduduk,
kk: this.form.kk,
miskin: this.form.miskin,
tahun: this.form.tahun,
};
const cek = templateDataBanjar.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kependudukan/databanjar/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dataBanjar.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data banjar");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataBanjar.delete.loading = true;
const response = await fetch(
`/api/kependudukan/databanjar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data banjar berhasil dihapus");
await dataBanjar.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus data banjar");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus data banjar");
} finally {
dataBanjar.delete.loading = false;
}
},
},
});
export default dataBanjar;

View File

@@ -0,0 +1,197 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
import { toast } from "react-toastify";
import { z } from "zod";
const templateDistribusiAgama = z.object({
agama: z.string().min(1, "Agama harus diisi"),
jumlah: z.number().min(0, "Jumlah harus diisi"),
tahun: z.number().min(2000, "Tahun harus diisi"),
});
const distribusiAgama = proxy({
create: {
form: {
agama: "",
jumlah: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async create() {
const cek = templateDistribusiAgama.safeParse(distribusiAgama.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
distribusiAgama.create.loading = true;
const res = await ApiFetch.api.kependudukan.distribusiagama["create"].post(distribusiAgama.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Sukses menambahkan distribusi agama");
distribusiAgama.create.form = { agama: "", jumlah: 0, tahun: new Date().getFullYear() };
distribusiAgama.findMany.load();
return id;
}
}
toast.error("Gagal menambahkan data");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
distribusiAgama.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
distribusiAgama.findMany.loading = true;
distribusiAgama.findMany.page = page;
distribusiAgama.findMany.search = search;
try {
const query: any = { page, limit, tahun };
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.distribusiagama["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
distribusiAgama.findMany.data = res.data.data ?? [];
distribusiAgama.findMany.totalPages = res.data.totalPages ?? 1;
} else {
distribusiAgama.findMany.data = [];
distribusiAgama.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch distribusi agama paginated:", err);
distribusiAgama.findMany.data = [];
distribusiAgama.findMany.totalPages = 1;
} finally {
distribusiAgama.findMany.loading = false;
}
},
},
findUnique: {
data: null as any | null,
async load(id: string) {
try {
const res = await fetch(`/api/kependudukan/distribusiagama/${id}`);
if (res.ok) {
const data = await res.json();
distribusiAgama.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch distribusiAgama:", res.statusText);
distribusiAgama.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching distribusiAgama:", error);
distribusiAgama.findUnique.data = null;
}
},
},
update: {
id: "",
form: {
agama: "",
jumlah: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
agama: this.form.agama,
jumlah: this.form.jumlah,
tahun: this.form.tahun,
};
const cek = templateDistribusiAgama.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kependudukan/distribusiagama/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await distribusiAgama.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data distribusi agama");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
distribusiAgama.delete.loading = true;
const response = await fetch(
`/api/kependudukan/distribusiagama/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Distribusi agama berhasil dihapus");
await distribusiAgama.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus distribusi agama");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus distribusi agama");
} finally {
distribusiAgama.delete.loading = false;
}
},
},
});
export default distribusiAgama;

View File

@@ -0,0 +1,197 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
import { toast } from "react-toastify";
import { z } from "zod";
const templateDistribusiUmur = z.object({
rentangUmur: z.string().min(1, "Rentang umur harus diisi"),
jumlah: z.number().min(0, "Jumlah harus diisi"),
tahun: z.number().min(2000, "Tahun harus diisi"),
});
const distribusiUmur = proxy({
create: {
form: {
rentangUmur: "",
jumlah: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async create() {
const cek = templateDistribusiUmur.safeParse(distribusiUmur.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
distribusiUmur.create.loading = true;
const res = await ApiFetch.api.kependudukan.distribusiumur["create"].post(distribusiUmur.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Sukses menambahkan distribusi umur");
distribusiUmur.create.form = { rentangUmur: "", jumlah: 0, tahun: new Date().getFullYear() };
distribusiUmur.findMany.load();
return id;
}
}
toast.error("Gagal menambahkan data");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
distribusiUmur.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
distribusiUmur.findMany.loading = true;
distribusiUmur.findMany.page = page;
distribusiUmur.findMany.search = search;
try {
const query: any = { page, limit, tahun };
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.distribusiumur["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
distribusiUmur.findMany.data = res.data.data ?? [];
distribusiUmur.findMany.totalPages = res.data.totalPages ?? 1;
} else {
distribusiUmur.findMany.data = [];
distribusiUmur.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch distribusi umur paginated:", err);
distribusiUmur.findMany.data = [];
distribusiUmur.findMany.totalPages = 1;
} finally {
distribusiUmur.findMany.loading = false;
}
},
},
findUnique: {
data: null as any | null,
async load(id: string) {
try {
const res = await fetch(`/api/kependudukan/distribusiumur/${id}`);
if (res.ok) {
const data = await res.json();
distribusiUmur.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch distribusi umur:", res.statusText);
distribusiUmur.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching distribusi umur:", error);
distribusiUmur.findUnique.data = null;
}
},
},
update: {
id: "",
form: {
rentangUmur: "",
jumlah: 0,
tahun: new Date().getFullYear(),
},
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
rentangUmur: this.form.rentangUmur,
jumlah: this.form.jumlah,
tahun: this.form.tahun,
};
const cek = templateDistribusiUmur.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kependudukan/distribusiumur/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await distribusiUmur.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data distribusi umur");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
distribusiUmur.delete.loading = true;
const response = await fetch(
`/api/kependudukan/distribusiumur/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Distribusi umur berhasil dihapus");
await distribusiUmur.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus distribusi umur");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus distribusi umur");
} finally {
distribusiUmur.delete.loading = false;
}
},
},
});
export default distribusiUmur;

View File

@@ -0,0 +1,209 @@
import ApiFetch from "@/lib/api-fetch";
import { proxy } from "valtio";
import { toast } from "react-toastify";
import { z } from "zod";
const templateMigrasiPenduduk = z.object({
jenis: z.string().min(1, "Jenis migrasi harus diisi"),
nama: z.string().min(1, "Nama harus diisi"),
tanggal: z.string().min(1, "Tanggal harus diisi"),
asalTujuan: z.string().min(1, "Asal/Tujuan harus diisi"),
alasan: z.string().optional(),
jenisKelamin: z.string().optional(),
});
const migrasiPenduduk = proxy({
create: {
form: {
jenis: "",
nama: "",
tanggal: "",
asalTujuan: "",
alasan: "",
jenisKelamin: "",
},
loading: false,
async create() {
const cek = templateMigrasiPenduduk.safeParse(migrasiPenduduk.create.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
migrasiPenduduk.create.loading = true;
const res = await ApiFetch.api.kependudukan.migrasipenduduk["create"].post(migrasiPenduduk.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Sukses menambahkan data migrasi penduduk");
migrasiPenduduk.create.form = { jenis: "", nama: "", tanggal: "", asalTujuan: "", alasan: "", jenisKelamin: "" };
migrasiPenduduk.findMany.load();
return id;
}
}
toast.error("Gagal menambahkan data");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
migrasiPenduduk.create.loading = false;
}
},
},
findMany: {
data: null as any[] | null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "", tahun = new Date().getFullYear()) => {
migrasiPenduduk.findMany.loading = true;
migrasiPenduduk.findMany.page = page;
migrasiPenduduk.findMany.search = search;
try {
const query: any = { page, limit, tahun };
if (search) query.search = search;
const res = await ApiFetch.api.kependudukan.migrasipenduduk["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
migrasiPenduduk.findMany.data = res.data.data ?? [];
migrasiPenduduk.findMany.totalPages = res.data.totalPages ?? 1;
} else {
migrasiPenduduk.findMany.data = [];
migrasiPenduduk.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch migrasi penduduk paginated:", err);
migrasiPenduduk.findMany.data = [];
migrasiPenduduk.findMany.totalPages = 1;
} finally {
migrasiPenduduk.findMany.loading = false;
}
},
},
findUnique: {
data: null as any | null,
async load(id: string) {
try {
const res = await fetch(`/api/kependudukan/migrasipenduduk/${id}`);
if (res.ok) {
const data = await res.json();
migrasiPenduduk.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch migrasi penduduk:", res.statusText);
migrasiPenduduk.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching migrasi penduduk:", error);
migrasiPenduduk.findUnique.data = null;
}
},
},
update: {
id: "",
form: {
jenis: "",
nama: "",
tanggal: "",
asalTujuan: "",
alasan: "",
jenisKelamin: "",
},
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
jenis: this.form.jenis,
nama: this.form.nama,
tanggal: this.form.tanggal,
asalTujuan: this.form.asalTujuan,
alasan: this.form.alasan,
jenisKelamin: this.form.jenisKelamin,
};
const cek = templateMigrasiPenduduk.safeParse(formData);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => `${v.path.join(".")}`).join("\n")}] required`;
toast.error(err);
return null;
}
try {
this.loading = true;
const res = await fetch(`/api/kependudukan/migrasipenduduk/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const result = await res.json();
if (!res.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await migrasiPenduduk.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data migrasi penduduk");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
migrasiPenduduk.delete.loading = true;
const response = await fetch(
`/api/kependudukan/migrasipenduduk/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data migrasi penduduk berhasil dihapus");
await migrasiPenduduk.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus data migrasi penduduk");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus data migrasi penduduk");
} finally {
migrasiPenduduk.delete.loading = false;
}
},
},
});
export default migrasiPenduduk;

View File

@@ -0,0 +1,249 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
NumberInput,
TextInput
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import dataBanjar from '../../../_state/kependudukan/data-banjar';
interface FormData {
nama: string;
penduduk: number;
kk: number;
miskin: number;
tahun: number;
}
export default function EditDataBanjar() {
const router = useRouter();
const { id } = useParams() as { id: string };
const stateDataBanjar = useProxy(dataBanjar);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
nama: '',
penduduk: 0,
kk: 0,
miskin: 0,
tahun: new Date().getFullYear(),
});
const [originalData, setOriginalData] = useState<FormData>({
nama: '',
penduduk: 0,
kk: 0,
miskin: 0,
tahun: new Date().getFullYear(),
});
const currentYear = new Date().getFullYear();
const isFormValid = () => {
return (
formData.nama?.trim() !== '' &&
formData.penduduk !== null &&
formData.penduduk >= 0 &&
formData.kk !== null &&
formData.kk >= 0 &&
formData.miskin !== null &&
formData.miskin >= 0 &&
formData.tahun !== null
);
};
useEffect(() => {
if (!id) return;
const loadData = async () => {
try {
setIsSubmitting(true);
stateDataBanjar.update.id = id;
await stateDataBanjar.findUnique.load(id);
const data = stateDataBanjar.findUnique.data;
if (data) {
setFormData({
nama: data.nama ?? '',
penduduk: Number(data.penduduk ?? 0),
kk: Number(data.kk ?? 0),
miskin: Number(data.miskin ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
setOriginalData({
nama: data.nama ?? '',
penduduk: Number(data.penduduk ?? 0),
kk: Number(data.kk ?? 0),
miskin: Number(data.miskin ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
}
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
} finally {
setIsSubmitting(false);
}
};
loadData();
}, [id]);
const handleChange = useCallback(
(field: keyof FormData) =>
(value: any) => {
const val =
field === 'penduduk' || field === 'kk' || field === 'miskin' || field === 'tahun'
? Number(value || 0)
: value;
setFormData((prev) => ({ ...prev, [field]: val }));
},
[]
);
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
penduduk: Number(originalData.penduduk),
kk: Number(originalData.kk),
miskin: Number(originalData.miskin),
tahun: Number(originalData.tahun),
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateDataBanjar.update.id = id;
stateDataBanjar.update.form = { ...formData };
await stateDataBanjar.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/kependudukan/data-banjar');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
} 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 Data Banjar
</Title>
</Group>
{/* Form Card */}
<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
label="Nama Banjar"
placeholder="Masukkan nama banjar"
value={formData.nama}
onChange={handleChange('nama')}
required
/>
<NumberInput
label="Jumlah Penduduk"
placeholder="Masukkan jumlah penduduk"
value={formData.penduduk}
onChange={handleChange('penduduk')}
min={0}
required
/>
<NumberInput
label="Jumlah KK"
placeholder="Masukkan jumlah KK"
value={formData.kk}
onChange={handleChange('kk')}
min={0}
required
/>
<NumberInput
label="Jumlah Penduduk Miskin"
placeholder="Masukkan jumlah penduduk miskin"
value={formData.miskin}
onChange={handleChange('miskin')}
min={0}
required
/>
<NumberInput
label="Tahun"
placeholder="Masukkan tahun"
value={formData.tahun}
onChange={handleChange('tahun')}
min={2000}
max={currentYear + 1}
required
/>
<Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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>
);
}

View File

@@ -0,0 +1,189 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
NumberInput,
TextInput
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import dataBanjar from '../../../_state/kependudukan/data-banjar';
import { toast } from 'react-toastify';
function CreateDataBanjar() {
const stateDataBanjar = useProxy(dataBanjar);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
value: String(currentYear - i),
label: String(currentYear - i),
}));
const isFormValid = () => {
return (
stateDataBanjar.create.form.nama?.trim() !== '' &&
stateDataBanjar.create.form.penduduk !== null &&
stateDataBanjar.create.form.penduduk >= 0 &&
stateDataBanjar.create.form.kk !== null &&
stateDataBanjar.create.form.kk >= 0 &&
stateDataBanjar.create.form.miskin !== null &&
stateDataBanjar.create.form.miskin >= 0 &&
stateDataBanjar.create.form.tahun !== null
);
};
const resetForm = () => {
stateDataBanjar.create.form = {
nama: '',
penduduk: 0,
kk: 0,
miskin: 0,
tahun: currentYear,
};
};
const handleSubmit = async () => {
try {
const id = await stateDataBanjar.create.create();
if (id) {
resetForm();
router.push('/admin/kependudukan/data-banjar');
}
} catch (error) {
console.error('Error creating data banjar:', error);
toast.error('Terjadi kesalahan saat menambah data banjar');
} 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">
Tambah Data Banjar
</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
label="Nama Banjar"
placeholder="Masukkan nama banjar"
value={stateDataBanjar.create.form.nama}
onChange={(e) => {
stateDataBanjar.create.form.nama = e.currentTarget.value;
}}
required
/>
<NumberInput
label="Jumlah Penduduk"
placeholder="Masukkan jumlah penduduk"
value={stateDataBanjar.create.form.penduduk}
onChange={(val) => {
stateDataBanjar.create.form.penduduk = Number(val || 0);
}}
min={0}
required
/>
<NumberInput
label="Jumlah KK"
placeholder="Masukkan jumlah KK"
value={stateDataBanjar.create.form.kk}
onChange={(val) => {
stateDataBanjar.create.form.kk = Number(val || 0);
}}
min={0}
required
/>
<NumberInput
label="Jumlah Penduduk Miskin"
placeholder="Masukkan jumlah penduduk miskin"
value={stateDataBanjar.create.form.miskin}
onChange={(val) => {
stateDataBanjar.create.form.miskin = Number(val || 0);
}}
min={0}
required
/>
<NumberInput
label="Tahun"
placeholder="Masukkan tahun"
value={stateDataBanjar.create.form.tahun}
onChange={(val) => {
stateDataBanjar.create.form.tahun = Number(val || currentYear);
}}
min={2000}
max={currentYear + 1}
required
/>
<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"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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 CreateDataBanjar;

View File

@@ -0,0 +1,304 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import dataBanjar from '../../_state/kependudukan/data-banjar';
function DataBanjarAdmin() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Data Banjar'
placeholder='Cari nama banjar...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDataBanjar search={search} />
</Box>
);
}
function ListDataBanjar({ search }: { search: string }) {
type DataBanjarType = {
id: string;
nama: string;
penduduk: number;
kk: number;
miskin: number;
tahun: number;
};
const router = useRouter();
const stateDataBanjar = useProxy(dataBanjar);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateDataBanjar.findMany;
const handleDelete = () => {
if (selectedId) {
stateDataBanjar.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
List Data Banjar
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kependudukan/data-banjar/create')}
fz={{ base: 'sm', md: 'md' }}
>
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%' }}>Nama Banjar</TableTh>
<TableTh style={{ width: '15%' }}>Penduduk</TableTh>
<TableTh style={{ width: '15%' }}>KK</TableTh>
<TableTh style={{ width: '15%' }}>Miskin</TableTh>
<TableTh style={{ width: '10%' }}>Tahun</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item: DataBanjarType) => (
<TableTr key={item.id}>
<TableTd>{item.nama}</TableTd>
<TableTd>{item.penduduk.toLocaleString('id-ID')}</TableTd>
<TableTd>{item.kk.toLocaleString('id-ID')}</TableTd>
<TableTd>{item.miskin.toLocaleString('id-ID')}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/data-banjar/${item.id}`)
}
fz="sm"
px="xs"
py="xs"
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDataBanjar.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="sm"
px="xs"
py="xs"
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={7}>
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data banjar yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item: DataBanjarType) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Banjar
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah Penduduk
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.penduduk.toLocaleString('id-ID')}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
KK
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.kk.toLocaleString('id-ID')}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Penduduk Miskin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.miskin.toLocaleString('id-ID')}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/data-banjar/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateDataBanjar.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data banjar 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="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data banjar ini?"
/>
</Box>
);
}
export default DataBanjarAdmin;

View File

@@ -0,0 +1,232 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title,
NumberInput,
Select
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import distribusiAgama from '../../../_state/kependudukan/distribusi-agama';
interface FormData {
agama: string;
jumlah: number;
tahun: number;
}
export default function EditDistribusiAgama() {
const router = useRouter();
const { id } = useParams() as { id: string };
const stateDistribusiAgama = useProxy(distribusiAgama);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
agama: '',
jumlah: 0,
tahun: new Date().getFullYear(),
});
const [originalData, setOriginalData] = useState<FormData>({
agama: '',
jumlah: 0,
tahun: new Date().getFullYear(),
});
const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
value: String(currentYear - i),
label: String(currentYear - i),
}));
const agamaOptions = [
{ value: 'HINDU', label: 'Hindu' },
{ value: 'ISLAM', label: 'Islam' },
{ value: 'KRISTEN', label: 'Kristen' },
{ value: 'KRISTEN_PROTESTAN', label: 'Kristen Protestan' },
{ value: 'KRISTEN_KATOLIK', label: 'Kristen Katolik' },
{ value: 'BUDDHA', label: 'Buddha' },
{ value: 'KONGHUCU', label: 'Konghucu' },
{ value: 'LAINNYA', label: 'Lainnya' },
];
const isFormValid = () => {
return (
formData.agama?.trim() !== '' &&
formData.jumlah !== null &&
formData.jumlah >= 0 &&
formData.tahun !== null
);
};
useEffect(() => {
if (!id) return;
const loadData = async () => {
try {
setIsSubmitting(true);
stateDistribusiAgama.update.id = id;
await stateDistribusiAgama.findUnique.load(id);
const data = stateDistribusiAgama.findUnique.data;
if (data) {
setFormData({
agama: data.agama ?? '',
jumlah: Number(data.jumlah ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
setOriginalData({
agama: data.agama ?? '',
jumlah: Number(data.jumlah ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
}
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
} finally {
setIsSubmitting(false);
}
};
loadData();
}, [id]);
const handleChange = useCallback(
(field: keyof FormData) =>
(value: any) => {
const val =
field === 'jumlah' || field === 'tahun'
? Number(value || 0)
: value;
setFormData((prev) => ({ ...prev, [field]: val }));
},
[]
);
const handleResetForm = () => {
setFormData({
agama: originalData.agama,
jumlah: Number(originalData.jumlah),
tahun: Number(originalData.tahun),
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateDistribusiAgama.update.id = id;
stateDistribusiAgama.update.form = { ...formData };
await stateDistribusiAgama.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/kependudukan/distribusi-agama');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
} 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 Distribusi Agama
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Agama"
placeholder="Pilih agama"
data={agamaOptions}
value={formData.agama}
onChange={handleChange('agama')}
required
searchable
/>
<NumberInput
label="Jumlah"
placeholder="Masukkan jumlah"
value={formData.jumlah}
onChange={handleChange('jumlah')}
min={0}
required
/>
<Select
label="Tahun"
placeholder="Pilih tahun"
data={yearOptions}
value={String(formData.tahun)}
onChange={handleChange('tahun')}
required
/>
<Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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>
);
}

View File

@@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title,
NumberInput,
Select
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import distribusiAgama from '../../../_state/kependudukan/distribusi-agama';
import { toast } from 'react-toastify';
function CreateDistribusiAgama() {
const stateDistribusiAgama = useProxy(distribusiAgama);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const agamaOptions = [
{ value: 'HINDU', label: 'Hindu' },
{ value: 'ISLAM', label: 'Islam' },
{ value: 'KRISTEN', label: 'Kristen' },
{ value: 'KRISTEN_PROTESTAN', label: 'Kristen Protestan' },
{ value: 'KRISTEN_KATOLIK', label: 'Kristen Katolik' },
{ value: 'BUDDHA', label: 'Buddha' },
{ value: 'KONGHUCU', label: 'Konghucu' },
{ value: 'LAINNYA', label: 'Lainnya' },
];
const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
value: String(currentYear - i),
label: String(currentYear - i),
}));
const isFormValid = () => {
return (
stateDistribusiAgama.create.form.agama?.trim() !== '' &&
stateDistribusiAgama.create.form.jumlah !== null &&
stateDistribusiAgama.create.form.jumlah >= 0 &&
stateDistribusiAgama.create.form.tahun !== null
);
};
const resetForm = () => {
stateDistribusiAgama.create.form = {
agama: '',
jumlah: 0,
tahun: currentYear,
};
};
const handleSubmit = async () => {
try {
const id = await stateDistribusiAgama.create.create();
if (id) {
resetForm();
router.push('/admin/kependudukan/distribusi-agama');
}
} catch (error) {
console.error('Error creating distribusi agama:', error);
toast.error('Terjadi kesalahan saat menambah distribusi agama');
} 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">
Tambah Distribusi Agama
</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">
<Select
label="Agama"
placeholder="Pilih agama"
data={agamaOptions}
value={stateDistribusiAgama.create.form.agama}
onChange={(val) => {
stateDistribusiAgama.create.form.agama = val || '';
}}
required
searchable
/>
<NumberInput
label="Jumlah"
placeholder="Masukkan jumlah"
value={stateDistribusiAgama.create.form.jumlah}
onChange={(val) => {
stateDistribusiAgama.create.form.jumlah = Number(val || 0);
}}
min={0}
required
/>
<Select
label="Tahun"
placeholder="Pilih tahun"
data={yearOptions}
value={String(stateDistribusiAgama.create.form.tahun)}
onChange={(val) => {
stateDistribusiAgama.create.form.tahun = Number(val || currentYear);
}}
required
/>
<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"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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 CreateDistribusiAgama;

View File

@@ -0,0 +1,283 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Flex,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
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 HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import distribusiAgama from '../../_state/kependudukan/distribusi-agama';
function DistribusiAgamaAdmin() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Distribusi Agama'
placeholder='Cari agama...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDistribusiAgama search={search} />
</Box>
);
}
function ListDistribusiAgama({ search }: { search: string }) {
type DistribusiAgamaType = {
id: string;
agama: string;
jumlah: number;
tahun: number;
};
const router = useRouter();
const stateDistribusiAgama = useProxy(distribusiAgama);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateDistribusiAgama.findMany;
const handleDelete = () => {
if (selectedId) {
stateDistribusiAgama.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
List Distribusi Agama
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kependudukan/distribusi-agama/create')}
fz={{ base: 'sm', md: 'md' }}
>
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: '40%' }}>Agama</TableTh>
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<TableTh style={{ width: '20%' }}>Tahun</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item: DistribusiAgamaType) => (
<TableTr key={item.id}>
<TableTd>{item.agama}</TableTd>
<TableTd>{item.jumlah.toLocaleString('id-ID')}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/distribusi-agama/${item.id}`)
}
fz="sm"
px="xs"
py="xs"
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDistribusiAgama.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="sm"
px="xs"
py="xs"
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data distribusi agama yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item: DistribusiAgamaType) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Agama
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.agama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jumlah.toLocaleString('id-ID')}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/distribusi-agama/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateDistribusiAgama.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data distribusi agama 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="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data distribusi agama ini?"
/>
</Box>
);
}
export default DistribusiAgamaAdmin;

View File

@@ -0,0 +1,232 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
NumberInput,
Select
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import distribusiUmur from '../../../_state/kependudukan/distribusi-umur';
interface FormData {
rentangUmur: string;
jumlah: number;
tahun: number;
}
export default function EditDistribusiUmur() {
const router = useRouter();
const { id } = useParams() as { id: string };
const stateDistribusiUmur = useProxy(distribusiUmur);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
rentangUmur: '',
jumlah: 0,
tahun: new Date().getFullYear(),
});
const [originalData, setOriginalData] = useState<FormData>({
rentangUmur: '',
jumlah: 0,
tahun: new Date().getFullYear(),
});
const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
value: String(currentYear - i),
label: String(currentYear - i),
}));
const rentangUmurOptions = [
{ value: '0-5', label: '0-5 Tahun' },
{ value: '6-12', label: '6-12 Tahun' },
{ value: '13-17', label: '13-17 Tahun' },
{ value: '18-25', label: '18-25 Tahun' },
{ value: '26-35', label: '26-35 Tahun' },
{ value: '36-45', label: '36-45 Tahun' },
{ value: '46-55', label: '46-55 Tahun' },
{ value: '56-65', label: '56-65 Tahun' },
{ value: '65+', label: '65+ Tahun' },
];
const isFormValid = () => {
return (
formData.rentangUmur?.trim() !== '' &&
formData.jumlah !== null &&
formData.jumlah >= 0 &&
formData.tahun !== null
);
};
useEffect(() => {
if (!id) return;
const loadData = async () => {
try {
setIsSubmitting(true);
stateDistribusiUmur.update.id = id;
await stateDistribusiUmur.findUnique.load(id);
const data = stateDistribusiUmur.findUnique.data;
if (data) {
setFormData({
rentangUmur: data.rentangUmur ?? '',
jumlah: Number(data.jumlah ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
setOriginalData({
rentangUmur: data.rentangUmur ?? '',
jumlah: Number(data.jumlah ?? 0),
tahun: Number(data.tahun ?? currentYear),
});
}
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
} finally {
setIsSubmitting(false);
}
};
loadData();
}, [id]);
const handleChange = useCallback(
(field: keyof FormData) =>
(value: any) => {
const val =
field === 'jumlah' || field === 'tahun'
? Number(value || 0)
: value;
setFormData((prev) => ({ ...prev, [field]: val }));
},
[]
);
const handleResetForm = () => {
setFormData({
rentangUmur: originalData.rentangUmur,
jumlah: Number(originalData.jumlah),
tahun: Number(originalData.tahun),
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateDistribusiUmur.update.id = id;
stateDistribusiUmur.update.form = { ...formData };
await stateDistribusiUmur.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/kependudukan/distribusi-umur');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
} 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 Distribusi Umur
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Rentang Umur"
placeholder="Pilih rentang umur"
data={rentangUmurOptions}
value={formData.rentangUmur}
onChange={handleChange('rentangUmur')}
required
searchable
/>
<NumberInput
label="Jumlah"
placeholder="Masukkan jumlah"
value={formData.jumlah}
onChange={handleChange('jumlah')}
min={0}
required
/>
<Select
label="Tahun"
placeholder="Pilih tahun"
data={yearOptions}
value={String(formData.tahun)}
onChange={handleChange('tahun')}
required
/>
<Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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>
);
}

View File

@@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
NumberInput,
Select
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import distribusiUmur from '../../../_state/kependudukan/distribusi-umur';
import { toast } from 'react-toastify';
function CreateDistribusiUmur() {
const stateDistribusiUmur = useProxy(distribusiUmur);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const rentangUmurOptions = [
{ value: '0-5', label: '0-5 Tahun' },
{ value: '6-12', label: '6-12 Tahun' },
{ value: '13-17', label: '13-17 Tahun' },
{ value: '18-25', label: '18-25 Tahun' },
{ value: '26-35', label: '26-35 Tahun' },
{ value: '36-45', label: '36-45 Tahun' },
{ value: '46-55', label: '46-55 Tahun' },
{ value: '56-65', label: '56-65 Tahun' },
{ value: '65+', label: '65+ Tahun' },
];
const currentYear = new Date().getFullYear();
const yearOptions = Array.from({ length: 10 }, (_, i) => ({
value: String(currentYear - i),
label: String(currentYear - i),
}));
const isFormValid = () => {
return (
stateDistribusiUmur.create.form.rentangUmur?.trim() !== '' &&
stateDistribusiUmur.create.form.jumlah !== null &&
stateDistribusiUmur.create.form.jumlah >= 0 &&
stateDistribusiUmur.create.form.tahun !== null
);
};
const resetForm = () => {
stateDistribusiUmur.create.form = {
rentangUmur: '',
jumlah: 0,
tahun: currentYear,
};
};
const handleSubmit = async () => {
try {
const id = await stateDistribusiUmur.create.create();
if (id) {
resetForm();
router.push('/admin/kependudukan/distribusi-umur');
}
} catch (error) {
console.error('Error creating distribusi umur:', error);
toast.error('Terjadi kesalahan saat menambah distribusi umur');
} 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">
Tambah Distribusi Umur
</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">
<Select
label="Rentang Umur"
placeholder="Pilih rentang umur"
data={rentangUmurOptions}
value={stateDistribusiUmur.create.form.rentangUmur}
onChange={(val) => {
stateDistribusiUmur.create.form.rentangUmur = val || '';
}}
required
searchable
/>
<NumberInput
label="Jumlah"
placeholder="Masukkan jumlah"
value={stateDistribusiUmur.create.form.jumlah}
onChange={(val) => {
stateDistribusiUmur.create.form.jumlah = Number(val || 0);
}}
min={0}
required
/>
<Select
label="Tahun"
placeholder="Pilih tahun"
data={yearOptions}
value={String(stateDistribusiUmur.create.form.tahun)}
onChange={(val) => {
stateDistribusiUmur.create.form.tahun = Number(val || currentYear);
}}
required
/>
<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"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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 CreateDistribusiUmur;

View File

@@ -0,0 +1,284 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Flex,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import distribusiUmur from '../../_state/kependudukan/distribusi-umur';
function DistribusiUmurAdmin() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Distribusi Umur'
placeholder='Cari rentang umur...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListDistribusiUmur search={search} />
</Box>
);
}
function ListDistribusiUmur({ search }: { search: string }) {
type DistribusiUmurType = {
id: string;
rentangUmur: string;
jumlah: number;
tahun: number;
};
const router = useRouter();
const stateDistribusiUmur = useProxy(distribusiUmur);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateDistribusiUmur.findMany;
const handleDelete = () => {
if (selectedId) {
stateDistribusiUmur.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
List Distribusi Umur
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kependudukan/distribusi-umur/create')}
fz={{ base: 'sm', md: 'md' }}
>
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: '40%' }}>Rentang Umur</TableTh>
<TableTh style={{ width: '20%' }}>Jumlah</TableTh>
<TableTh style={{ width: '20%' }}>Tahun</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item: DistribusiUmurType) => (
<TableTr key={item.id}>
<TableTd>{item.rentangUmur}</TableTd>
<TableTd>{item.jumlah.toLocaleString('id-ID')}</TableTd>
<TableTd>{item.tahun}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/distribusi-umur/${item.id}`)
}
fz="sm"
px="xs"
py="xs"
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDistribusiUmur.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="sm"
px="xs"
py="xs"
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={5}>
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data distribusi umur yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item: DistribusiUmurType) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Rentang Umur
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.rentangUmur}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jumlah
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jumlah.toLocaleString('id-ID')}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tahun
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.tahun}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/distribusi-umur/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateDistribusiUmur.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data distribusi umur 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="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data distribusi umur ini?"
/>
</Box>
);
}
export default DistribusiUmurAdmin;

View File

@@ -0,0 +1,267 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
TextInput,
Select,
Textarea
} from '@mantine/core';
import { DatePickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import migrasiPenduduk from '../../../_state/kependudukan/migrasi-penduduk';
interface FormData {
jenis: string;
nama: string;
tanggal: string;
asalTujuan: string;
alasan: string;
jenisKelamin: string;
}
export default function EditMigrasiPenduduk() {
const router = useRouter();
const { id } = useParams() as { id: string };
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
jenis: '',
nama: '',
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
});
const [originalData, setOriginalData] = useState<FormData>({
jenis: '',
nama: '',
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
});
const jenisOptions = [
{ value: 'MASUK', label: 'Masuk' },
{ value: 'KELUAR', label: 'Keluar' },
];
const jenisKelaminOptions = [
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
];
const isFormValid = () => {
return (
formData.jenis?.trim() !== '' &&
formData.nama?.trim() !== '' &&
formData.tanggal?.trim() !== '' &&
formData.asalTujuan?.trim() !== ''
);
};
useEffect(() => {
if (!id) return;
const loadData = async () => {
try {
setIsSubmitting(true);
stateMigrasiPenduduk.update.id = id;
await stateMigrasiPenduduk.findUnique.load(id);
const data = stateMigrasiPenduduk.findUnique.data;
if (data) {
setFormData({
jenis: data.jenis ?? '',
nama: data.nama ?? '',
tanggal: data.tanggal ?? '',
asalTujuan: data.asalTujuan ?? '',
alasan: data.alasan ?? '',
jenisKelamin: data.jenisKelamin ?? '',
});
setOriginalData({
jenis: data.jenis ?? '',
nama: data.nama ?? '',
tanggal: data.tanggal ?? '',
asalTujuan: data.asalTujuan ?? '',
alasan: data.alasan ?? '',
jenisKelamin: data.jenisKelamin ?? '',
});
}
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
} finally {
setIsSubmitting(false);
}
};
loadData();
}, [id]);
const handleChange = useCallback(
(field: keyof FormData) =>
(value: any) => {
const val = value || '';
setFormData((prev) => ({ ...prev, [field]: val }));
},
[]
);
const handleResetForm = () => {
setFormData({
jenis: originalData.jenis,
nama: originalData.nama,
tanggal: originalData.tanggal,
asalTujuan: originalData.asalTujuan,
alasan: originalData.alasan,
jenisKelamin: originalData.jenisKelamin,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateMigrasiPenduduk.update.id = id;
stateMigrasiPenduduk.update.form = { ...formData };
await stateMigrasiPenduduk.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/kependudukan/migrasi-penduduk');
} catch (error) {
console.error('Error updating data:', error);
toast.error('Gagal memperbarui data');
} 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 Migrasi Penduduk
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select
label="Jenis Migrasi"
placeholder="Pilih jenis migrasi"
data={jenisOptions}
value={formData.jenis}
onChange={handleChange('jenis')}
required
/>
<TextInput
label="Nama"
placeholder="Masukkan nama lengkap"
value={formData.nama}
onChange={handleChange('nama')}
required
/>
<DatePickerInput
label="Tanggal"
placeholder="Pilih tanggal"
value={formData.tanggal ? new Date(formData.tanggal) : null}
onChange={(val: string | null) => {
setFormData((prev) => ({
...prev,
tanggal: val || '',
}));
}}
required
/>
<TextInput
label={formData.jenis === 'MASUK' ? 'Asal' : 'Tujuan'}
placeholder={formData.jenis === 'MASUK' ? 'Masukkan asal' : 'Masukkan tujuan'}
value={formData.asalTujuan}
onChange={handleChange('asalTujuan')}
required
/>
<Textarea
label="Alasan"
placeholder="Masukkan alasan (opsional)"
value={formData.alasan}
onChange={handleChange('alasan')}
autosize
minRows={2}
/>
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={jenisKelaminOptions}
value={formData.jenisKelamin}
onChange={handleChange('jenisKelamin')}
/>
<Group justify="flex-end">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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>
);
}

View File

@@ -0,0 +1,199 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
Title,
TextInput,
Select,
Textarea
} from '@mantine/core';
import { DatePickerInput } from '@mantine/dates';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import migrasiPenduduk from '../../../_state/kependudukan/migrasi-penduduk';
import { toast } from 'react-toastify';
function CreateMigrasiPenduduk() {
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const jenisOptions = [
{ value: 'MASUK', label: 'Masuk' },
{ value: 'KELUAR', label: 'Keluar' },
];
const jenisKelaminOptions = [
{ value: 'L', label: 'Laki-laki' },
{ value: 'P', label: 'Perempuan' },
];
const isFormValid = () => {
return (
stateMigrasiPenduduk.create.form.jenis?.trim() !== '' &&
stateMigrasiPenduduk.create.form.nama?.trim() !== '' &&
stateMigrasiPenduduk.create.form.tanggal?.trim() !== '' &&
stateMigrasiPenduduk.create.form.asalTujuan?.trim() !== ''
);
};
const resetForm = () => {
stateMigrasiPenduduk.create.form = {
jenis: '',
nama: '',
tanggal: '',
asalTujuan: '',
alasan: '',
jenisKelamin: '',
};
};
const handleSubmit = async () => {
try {
const id = await stateMigrasiPenduduk.create.create();
if (id) {
resetForm();
router.push('/admin/kependudukan/migrasi-penduduk');
}
} catch (error) {
console.error('Error creating migrasi penduduk:', error);
toast.error('Terjadi kesalahan saat menambah data migrasi penduduk');
} 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">
Tambah Migrasi Penduduk
</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">
<Select
label="Jenis Migrasi"
placeholder="Pilih jenis migrasi"
data={jenisOptions}
value={stateMigrasiPenduduk.create.form.jenis}
onChange={(val) => {
stateMigrasiPenduduk.create.form.jenis = val || '';
}}
required
/>
<TextInput
label="Nama"
placeholder="Masukkan nama lengkap"
value={stateMigrasiPenduduk.create.form.nama}
onChange={(e) => {
stateMigrasiPenduduk.create.form.nama = e.currentTarget.value;
}}
required
/>
<DatePickerInput
label="Tanggal"
placeholder="Pilih tanggal"
value={stateMigrasiPenduduk.create.form.tanggal ? new Date(stateMigrasiPenduduk.create.form.tanggal) : null}
onChange={(val: string | null) => {
stateMigrasiPenduduk.create.form.tanggal = val || '';
}}
required
/>
<TextInput
label={stateMigrasiPenduduk.create.form.jenis === 'MASUK' ? 'Asal' : 'Tujuan'}
placeholder={stateMigrasiPenduduk.create.form.jenis === 'MASUK' ? 'Masukkan asal' : 'Masukkan tujuan'}
value={stateMigrasiPenduduk.create.form.asalTujuan}
onChange={(e) => {
stateMigrasiPenduduk.create.form.asalTujuan = e.currentTarget.value;
}}
required
/>
<Textarea
label="Alasan"
placeholder="Masukkan alasan (opsional)"
value={stateMigrasiPenduduk.create.form.alasan}
onChange={(e) => {
stateMigrasiPenduduk.create.form.alasan = e.currentTarget.value;
}}
autosize
minRows={2}
/>
<Select
label="Jenis Kelamin"
placeholder="Pilih jenis kelamin"
data={jenisKelaminOptions}
value={stateMigrasiPenduduk.create.form.jenisKelamin}
onChange={(val) => {
stateMigrasiPenduduk.create.form.jenisKelamin = val || '';
}}
/>
<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"
disabled={!isFormValid() || isSubmitting}
style={{
background: !isFormValid() || isSubmitting
? `linear-gradient(135deg, #cccccc, #eeeeee)`
: `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 CreateMigrasiPenduduk;

View File

@@ -0,0 +1,339 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'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 { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import migrasiPenduduk from '../../_state/kependudukan/migrasi-penduduk';
function MigrasiPendudukAdmin() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Migrasi Penduduk'
placeholder='Cari nama...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.currentTarget.value)}
/>
<ListMigrasiPenduduk search={search} />
</Box>
);
}
function ListMigrasiPenduduk({ search }: { search: string }) {
type MigrasiPendudukType = {
id: string;
jenis: string;
nama: string;
tanggal: string;
asalTujuan: string;
alasan: string | null;
jenisKelamin: string | null;
};
const router = useRouter();
const stateMigrasiPenduduk = useProxy(migrasiPenduduk);
const [modalHapus, setModalHapus] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 1000);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateMigrasiPenduduk.findMany;
const handleDelete = () => {
if (selectedId) {
stateMigrasiPenduduk.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
};
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
const formatTanggal = (tanggal: string) => {
try {
return new Date(tanggal).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
} catch {
return tanggal;
}
};
return (
<Box py={{ base: 'sm', md: 'md' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'sm', md: 'md' }}>
<Title order={4} lh={{ base: 1.2, md: 1.15 }}>
List Migrasi Penduduk
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/kependudukan/migrasi-penduduk/create')}
fz={{ base: 'sm', md: 'md' }}
>
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: '10%' }}>Jenis</TableTh>
<TableTh style={{ width: '20%' }}>Nama</TableTh>
<TableTh style={{ width: '12%' }}>Tanggal</TableTh>
<TableTh style={{ width: '20%' }}>Asal/Tujuan</TableTh>
<TableTh style={{ width: '10%' }}>L/P</TableTh>
<TableTh style={{ width: '10%' }}>Edit</TableTh>
<TableTh style={{ width: '10%' }}>Hapus</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item: MigrasiPendudukType) => (
<TableTr key={item.id}>
<TableTd>
<Text
fz="sm"
fw={500}
c={item.jenis === 'MASUK' ? 'green' : 'red'}
>
{item.jenis}
</Text>
</TableTd>
<TableTd>{item.nama}</TableTd>
<TableTd>{formatTanggal(item.tanggal)}</TableTd>
<TableTd>{item.asalTujuan}</TableTd>
<TableTd>{item.jenisKelamin || '-'}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/migrasi-penduduk/${item.id}`)
}
fz="sm"
px="xs"
py="xs"
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateMigrasiPenduduk.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="sm"
px="xs"
py="xs"
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={7}>
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data migrasi penduduk yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Card */}
<Box hiddenFrom="md">
<Stack gap="xs">
{filteredData.length > 0 ? (
filteredData.map((item: MigrasiPendudukType) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jenis Migrasi
</Text>
<Text
fz="sm"
fw={500}
c={item.jenis === 'MASUK' ? 'green' : 'red'}
>
{item.jenis}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.nama}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Tanggal
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{formatTanggal(item.tanggal)}
</Text>
</Box>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Asal/Tujuan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.asalTujuan}
</Text>
</Box>
{item.alasan && (
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Alasan
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.alasan}
</Text>
</Box>
)}
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Jenis Kelamin
</Text>
<Text fz="sm" fw={500} lh={1.4}>
{item.jenisKelamin || '-'}
</Text>
</Box>
<Group justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/kependudukan/migrasi-penduduk/${item.id}`)
}
fz="xs"
px="xs"
py="xs"
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
disabled={stateMigrasiPenduduk.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
fz="xs"
px="xs"
py="xs"
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={20}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data migrasi penduduk 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="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus data migrasi penduduk ini?"
/>
</Box>
);
}
export default MigrasiPendudukAdmin;

View File

@@ -373,6 +373,33 @@ export const devBar = [
}
]
},
{
id: "Kependudukan",
name: "Kependudukan",
path: "",
children: [
{
id: "Kependudukan_1",
name: "Distribusi Agama",
path: "/admin/kependudukan/distribusi-agama"
},
{
id: "Kependudukan_2",
name: "Distribusi Umur",
path: "/admin/kependudukan/distribusi-umur"
},
{
id: "Kependudukan_3",
name: "Data Banjar",
path: "/admin/kependudukan/data-banjar"
},
{
id: "Kependudukan_4",
name: "Migrasi Penduduk",
path: "/admin/kependudukan/migrasi-penduduk"
}
]
},
{
id: "Musik",
name: "Musik",
@@ -777,6 +804,33 @@ export const navBar = [
}
]
},
{
id: "Kependudukan",
name: "Kependudukan",
path: "",
children: [
{
id: "Kependudukan_1",
name: "Distribusi Agama",
path: "/admin/kependudukan/distribusi-agama"
},
{
id: "Kependudukan_2",
name: "Distribusi Umur",
path: "/admin/kependudukan/distribusi-umur"
},
{
id: "Kependudukan_3",
name: "Data Banjar",
path: "/admin/kependudukan/data-banjar"
},
{
id: "Kependudukan_4",
name: "Migrasi Penduduk",
path: "/admin/kependudukan/migrasi-penduduk"
}
]
},
{
id: "Musik",
name: "Musik",
@@ -1098,6 +1152,33 @@ export const role1 = [
path: "/admin/lingkungan/konservasi-adat-bali/filosofi-tri-hita-karana"
}
]
},
{
id: "Kependudukan",
name: "Kependudukan",
path: "",
children: [
{
id: "Kependudukan_1",
name: "Distribusi Agama",
path: "/admin/kependudukan/distribusi-agama"
},
{
id: "Kependudukan_2",
name: "Distribusi Umur",
path: "/admin/kependudukan/distribusi-umur"
},
{
id: "Kependudukan_3",
name: "Data Banjar",
path: "/admin/kependudukan/data-banjar"
},
{
id: "Kependudukan_4",
name: "Migrasi Penduduk",
path: "/admin/kependudukan/migrasi-penduduk"
}
]
},
{
id: "Musik",

View File

@@ -0,0 +1,10 @@
import Elysia from "elysia";
import dashboardSummary from "./summary";
const DashboardKependudukan = new Elysia({
prefix: "/dashboard",
tags: ["Kependudukan/Dashboard"],
})
.get("/summary", dashboardSummary)
export default DashboardKependudukan;

View File

@@ -0,0 +1,147 @@
import prisma from "@/lib/prisma";
export default async function dashboardSummary() {
try {
const currentYear = new Date().getFullYear();
// Get dashboard summary
const [
totalPenduduk,
totalKK,
totalKelahiran,
totalKemiskinan,
kelahiranData,
kematianData,
pindahMasukData,
pindahKeluarData,
agamaData,
umurData,
banjarData
] = await Promise.all([
// Total penduduk - hitung dari data banjar
prisma.dataBanjar.aggregate({
where: { isActive: true, tahun: currentYear },
_sum: { penduduk: true }
}),
// Total KK
prisma.dataBanjar.aggregate({
where: { isActive: true, tahun: currentYear },
_sum: { kk: true }
}),
// Total kelahiran tahun ini
prisma.kelahiran.count({
where: {
isActive: true,
tanggal: {
gte: new Date(`${currentYear}-01-01`),
lte: new Date(`${currentYear}-12-31`),
}
}
}),
// Total penduduk miskin
prisma.dataBanjar.aggregate({
where: { isActive: true, tahun: currentYear },
_sum: { miskin: true }
}),
// Kelahiran data
prisma.kelahiran.findMany({
where: {
isActive: true,
tanggal: {
gte: new Date(`${currentYear}-01-01`),
lte: new Date(`${currentYear}-12-31`),
}
},
orderBy: { tanggal: 'asc' }
}),
// Kematian data
prisma.kematian.findMany({
where: {
isActive: true,
tanggal: {
gte: new Date(`${currentYear}-01-01`),
lte: new Date(`${currentYear}-12-31`),
}
},
orderBy: { tanggal: 'asc' }
}),
// Pindah masuk
prisma.migrasiPenduduk.count({
where: {
isActive: true,
jenis: 'MASUK',
tanggal: {
gte: new Date(`${currentYear}-01-01`),
lte: new Date(`${currentYear}-12-31`),
}
}
}),
// Pindah keluar
prisma.migrasiPenduduk.count({
where: {
isActive: true,
jenis: 'KELUAR',
tanggal: {
gte: new Date(`${currentYear}-01-01`),
lte: new Date(`${currentYear}-12-31`),
}
}
}),
// Data agama
prisma.distribusiAgama.findMany({
where: { isActive: true, tahun: currentYear },
orderBy: { jumlah: 'desc' }
}),
// Data umur
prisma.distribusiUmur.findMany({
where: { isActive: true, tahun: currentYear },
orderBy: { createdAt: 'asc' }
}),
// Data banjar
prisma.dataBanjar.findMany({
where: { isActive: true, tahun: currentYear },
orderBy: { nama: 'asc' }
})
]);
return {
success: true,
message: "Dashboard summary berhasil diambil",
data: {
tahun: currentYear,
summary: {
totalPenduduk: totalPenduduk._sum.penduduk || 0,
totalKK: totalKK._sum.kk || 0,
totalKelahiran: totalKelahiran,
totalKemiskinan: totalKemiskinan._sum.miskin || 0,
},
dinamika: {
kelahiran: totalKelahiran,
kematian: kematianData.length,
pindahMasuk: pindahMasukData,
pindahKeluar: pindahKeluarData,
},
agama: agamaData,
umur: umurData,
banjar: banjarData,
}
};
} catch (error) {
console.error("Error fetching dashboard summary:", error);
return {
success: false,
message: "Terjadi kesalahan saat mengambil data dashboard",
data: null,
};
}
}

View File

@@ -0,0 +1,40 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DataBanjarGetPayload<{
select: {
nama: true;
penduduk: true;
kk: true;
miskin: true;
tahun: true;
}
}>
export default async function dataBanjarCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.dataBanjar.create({
data: {
nama: body.nama,
penduduk: body.penduduk,
kk: body.kk,
miskin: body.miskin,
tahun: body.tahun,
},
select: {
id: true,
nama: true,
penduduk: true,
kk: true,
miskin: true,
tahun: true,
}
});
return {
success: true,
message: "Sukses menambahkan data banjar",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataBanjarDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.dataBanjar.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.dataBanjar.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,49 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
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 skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.dataBanjar.findMany({
where,
skip,
take: limit,
orderBy: { nama: "asc" },
}),
prisma.dataBanjar.count({
where,
}),
]);
return {
success: true,
message: "Success fetch data banjar with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch data banjar with pagination",
data: null,
};
}
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function dataBanjarFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.dataBanjar.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Data ditemukan",
data: data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,43 @@
import Elysia, { t } from "elysia";
import dataBanjarFindUnique from "./findUnique";
import dataBanjarUpdate from "./updt";
import dataBanjarFindMany from "./findMany";
import dataBanjarCreate from "./create";
import dataBanjarDelete from "./del";
const DataBanjar = new Elysia({
prefix: "/databanjar",
tags: ["Kependudukan/Data Banjar"],
})
.get("/:id", async (context) => {
const response = await dataBanjarFindUnique(new Request(context.request))
return response
})
.get("/find-many", dataBanjarFindMany)
.post("/create", dataBanjarCreate, {
body: t.Object({
nama: t.String(),
penduduk: t.Number(),
kk: t.Number(),
miskin: t.Number(),
tahun: t.Number(),
}),
})
.put("/:id", dataBanjarUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
nama: t.String(),
penduduk: t.Number(),
kk: t.Number(),
miskin: t.Number(),
tahun: t.Number(),
}),
})
.delete("/del/:id", dataBanjarDelete, {
params: t.Object({
id: t.String(),
}),
})
export default DataBanjar;

View File

@@ -0,0 +1,51 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function dataBanjarUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const {nama, penduduk, kk, miskin, tahun} = context.body as {
nama: string;
penduduk: number;
kk: number;
miskin: number;
tahun: number;
}
const existing = await prisma.dataBanjar.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const updated = await prisma.dataBanjar.update({
where: { id },
data: {
nama,
penduduk,
kk,
miskin,
tahun,
},
})
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
}
}

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DistribusiAgamaGetPayload<{
select: {
agama: true;
jumlah: true;
tahun: true;
}
}>
export default async function distribusiAgamaCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.distribusiAgama.create({
data: {
agama: body.agama,
jumlah: body.jumlah,
tahun: body.tahun,
},
select: {
id: true,
agama: true,
jumlah: true,
tahun: true,
}
});
return {
success: true,
message: "Sukses menambahkan distribusi agama",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function distribusiAgamaDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.distribusiAgama.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.distribusiAgama.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
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 skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ agama: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.distribusiAgama.findMany({
where,
skip,
take: limit,
orderBy: { jumlah: "desc" },
}),
prisma.distribusiAgama.count({
where,
}),
]);
return {
success: true,
message: "Success fetch distribusi agama with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch distribusi agama with pagination",
data: null,
};
}
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function distribusiAgamaFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.distribusiAgama.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Data ditemukan",
data: data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import distribusiAgamaFindUnique from "./findUnique";
import distribusiAgamaUpdate from "./updt";
import distribusiAgamaFindMany from "./findMany";
import distribusiAgamaCreate from "./create";
import distribusiAgamaDelete from "./del";
const DistribusiAgama = new Elysia({
prefix: "/distribusiagama",
tags: ["Kependudukan/Distribusi Agama"],
})
.get("/:id", async (context) => {
const response = await distribusiAgamaFindUnique(new Request(context.request))
return response
})
.get("/find-many", distribusiAgamaFindMany)
.post("/create", distribusiAgamaCreate, {
body: t.Object({
agama: t.String(),
jumlah: t.Number(),
tahun: t.Number(),
}),
})
.put("/:id", distribusiAgamaUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
agama: t.String(),
jumlah: t.Number(),
tahun: t.Number(),
}),
})
.delete("/del/:id", distribusiAgamaDelete, {
params: t.Object({
id: t.String(),
}),
})
export default DistribusiAgama;

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function distribusiAgamaUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const {agama, jumlah, tahun} = context.body as {
agama: string;
jumlah: number;
tahun: number;
}
const existing = await prisma.distribusiAgama.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const updated = await prisma.distribusiAgama.update({
where: { id },
data: {
agama,
jumlah,
tahun,
},
})
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
}
}

View File

@@ -0,0 +1,34 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DistribusiUmurGetPayload<{
select: {
rentangUmur: true;
jumlah: true;
tahun: true;
}
}>
export default async function distribusiUmurCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.distribusiUmur.create({
data: {
rentangUmur: body.rentangUmur,
jumlah: body.jumlah,
tahun: body.tahun,
},
select: {
id: true,
rentangUmur: true,
jumlah: true,
tahun: true,
}
});
return {
success: true,
message: "Sukses menambahkan distribusi umur",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function distribusiUmurDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.distribusiUmur.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.distribusiUmur.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,42 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
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 skip = (page - 1) * limit;
const where: any = { isActive: true, tahun };
try {
const [data, total] = await Promise.all([
prisma.distribusiUmur.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.distribusiUmur.count({
where,
}),
]);
return {
success: true,
message: "Success fetch distribusi umur with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch distribusi umur with pagination",
data: null,
};
}
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function distribusiUmurFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.distribusiUmur.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Data ditemukan",
data: data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import distribusiUmurFindUnique from "./findUnique";
import distribusiUmurUpdate from "./updt";
import distribusiUmurFindMany from "./findMany";
import distribusiUmurCreate from "./create";
import distribusiUmurDelete from "./del";
const DistribusiUmur = new Elysia({
prefix: "/distribusiumur",
tags: ["Kependudukan/Distribusi Umur"],
})
.get("/:id", async (context) => {
const response = await distribusiUmurFindUnique(new Request(context.request))
return response
})
.get("/find-many", distribusiUmurFindMany)
.post("/create", distribusiUmurCreate, {
body: t.Object({
rentangUmur: t.String(),
jumlah: t.Number(),
tahun: t.Number(),
}),
})
.put("/:id", distribusiUmurUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
rentangUmur: t.String(),
jumlah: t.Number(),
tahun: t.Number(),
}),
})
.delete("/del/:id", distribusiUmurDelete, {
params: t.Object({
id: t.String(),
}),
})
export default DistribusiUmur;

View File

@@ -0,0 +1,47 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function distribusiUmurUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const {rentangUmur, jumlah, tahun} = context.body as {
rentangUmur: string;
jumlah: number;
tahun: number;
}
const existing = await prisma.distribusiUmur.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const updated = await prisma.distribusiUmur.update({
where: { id },
data: {
rentangUmur,
jumlah,
tahun,
},
})
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
}
}

View File

@@ -0,0 +1,18 @@
import Elysia from "elysia";
import DistribusiAgama from "./distribusi-agama";
import DistribusiUmur from "./distribusi-umur";
import DataBanjar from "./data-banjar";
import MigrasiPenduduk from "./migrasi-penduduk";
import DashboardKependudukan from "./dashboard";
const Kependudukan = new Elysia({
prefix: "/kependudukan",
tags: ["Kependudukan"],
})
.use(DashboardKependudukan)
.use(DistribusiAgama)
.use(DistribusiUmur)
.use(DataBanjar)
.use(MigrasiPenduduk)
export default Kependudukan;

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.MigrasiPendudukGetPayload<{
select: {
jenis: true;
nama: true;
tanggal: true;
asalTujuan: true;
alasan: true;
jenisKelamin: true;
}
}>
export default async function migrasiPendudukCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.migrasiPenduduk.create({
data: {
jenis: body.jenis,
nama: body.nama,
tanggal: new Date(body.tanggal),
asalTujuan: body.asalTujuan,
alasan: body.alasan,
jenisKelamin: body.jenisKelamin,
},
select: {
id: true,
jenis: true,
nama: true,
tanggal: true,
asalTujuan: true,
alasan: true,
jenisKelamin: true,
}
});
return {
success: true,
message: "Sukses menambahkan migrasi penduduk",
data: created,
};
}

View File

@@ -0,0 +1,36 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function migrasiPendudukDelete(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const existing = await prisma.migrasiPenduduk.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const deleted = await prisma.migrasiPenduduk.delete({
where: { id },
})
return {
success: true,
message: "Data berhasil dihapus",
data: deleted,
}
}

View File

@@ -0,0 +1,62 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function migrasiPendudukFindMany(context: Context) {
const page = Number(context.query.page) || 1;
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 skip = (page - 1) * limit;
const where: any = { isActive: true };
if (jenis) {
where.jenis = jenis;
}
if (tahun) {
where.tanggal = {
gte: new Date(`${tahun}-01-01`),
lte: new Date(`${tahun}-12-31`),
};
}
if (search) {
where.OR = [
{ nama: { contains: search, mode: "insensitive" } },
{ asalTujuan: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.migrasiPenduduk.findMany({
where,
skip,
take: limit,
orderBy: { tanggal: "desc" },
}),
prisma.migrasiPenduduk.count({
where,
}),
]);
return {
success: true,
message: "Success fetch migrasi penduduk with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch migrasi penduduk with pagination",
data: null,
};
}
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function migrasiPendudukFindUnique(request: Request) {
const url = new URL(request.url);
const pathSegments = url.pathname.split('/');
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json({
success: false,
message: "ID tidak boleh kosong",
}, { status: 400 });
}
try {
if (typeof id !== 'string') {
return Response.json({
success: false,
message: "ID tidak valid",
}, { status: 400 });
}
const data = await prisma.migrasiPenduduk.findUnique({
where: { id },
});
if (!data) {
return Response.json({
success: false,
message: "Data tidak ditemukan",
}, { status: 404 });
}
return Response.json({
success: true,
message: "Data ditemukan",
data: data,
}, { status: 200 });
} catch (error) {
console.error("Error fetching data:", error);
return Response.json({
success: false,
message: "Terjadi kesalahan saat mengambil data",
}, { status: 500 });
}
}

View File

@@ -0,0 +1,45 @@
import Elysia, { t } from "elysia";
import migrasiPendudukFindUnique from "./findUnique";
import migrasiPendudukUpdate from "./updt";
import migrasiPendudukFindMany from "./findMany";
import migrasiPendudukCreate from "./create";
import migrasiPendudukDelete from "./del";
const MigrasiPenduduk = new Elysia({
prefix: "/migrasipenduduk",
tags: ["Kependudukan/Migrasi Penduduk"],
})
.get("/:id", async (context) => {
const response = await migrasiPendudukFindUnique(new Request(context.request))
return response
})
.get("/find-many", migrasiPendudukFindMany)
.post("/create", migrasiPendudukCreate, {
body: t.Object({
jenis: t.String(),
nama: t.String(),
tanggal: t.String(),
asalTujuan: t.String(),
alasan: t.Optional(t.String()),
jenisKelamin: t.Optional(t.String()),
}),
})
.put("/:id", migrasiPendudukUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
jenis: t.String(),
nama: t.String(),
tanggal: t.String(),
asalTujuan: t.String(),
alasan: t.Optional(t.String()),
jenisKelamin: t.Optional(t.String()),
}),
})
.delete("/del/:id", migrasiPendudukDelete, {
params: t.Object({
id: t.String(),
}),
})
export default MigrasiPenduduk;

View File

@@ -0,0 +1,53 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function migrasiPendudukUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "ID tidak ditemukan",
}
}
const {jenis, nama, tanggal, asalTujuan, alasan, jenisKelamin} = context.body as {
jenis: string;
nama: string;
tanggal: string;
asalTujuan: string;
alasan?: string;
jenisKelamin?: string;
}
const existing = await prisma.migrasiPenduduk.findUnique({
where: {
id: id,
},
})
if (!existing) {
return {
success: false,
message: "Data tidak ditemukan",
}
}
const updated = await prisma.migrasiPenduduk.update({
where: { id },
data: {
jenis,
nama,
tanggal: new Date(tanggal),
asalTujuan,
alasan,
jenisKelamin,
},
})
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
}
}

View File

@@ -0,0 +1,196 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, Title, SimpleGrid, Skeleton, Group, Badge, Center, Image } from '@mantine/core';
import { IconUsers, IconHome, IconBasket, IconCoin, IconDatabaseOff } from '@tabler/icons-react';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import kependudukanDashboard from '@/app/admin/(dashboard)/_state/kependudukan/dashboard';
import { useShallowEffect } from '@mantine/hooks';
function Page() {
const state = useProxy(kependudukanDashboard)
useShallowEffect(() => {
state.summary.load()
}, [])
const summary = state.summary.data?.summary;
if (state.summary.loading) {
return (
<Stack py={10}>
<Skeleton h={200} />
</Stack>
)
}
if (!summary) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Dashboard Kependudukan
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Ringkasan data kependudukan Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Center py="xl">
<Stack align="center" gap="md">
<IconDatabaseOff size={80} color={colors.grey['2']} />
<Text ta="center" fz="lg" fw={500} c="dimmed">
Data Belum Tersedia
</Text>
<Text ta="center" fz="sm" c="dimmed" maw={400}>
Data kependudukan untuk tahun ini belum diperbarui.
Silakan hubungi administrator desa untuk informasi lebih lanjut.
</Text>
</Stack>
</Center>
</Paper>
</Box>
</Stack>
)
}
const stats = [
{
title: 'Total Penduduk',
value: summary.totalPenduduk,
icon: IconUsers,
color: colors['blue-button'],
},
{
title: 'Kepala Keluarga',
value: summary.totalKK,
icon: IconHome,
color: '#6EDF9C',
},
{
title: 'Kelahiran Tahun Ini',
value: summary.totalKelahiran,
icon: IconBasket,
color: '#FF9F43',
},
{
title: 'Penduduk Miskin',
value: summary.totalKemiskinan,
icon: IconCoin,
color: '#EE5050',
},
];
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Dashboard Kependudukan
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Ringkasan data kependudukan Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md">
{stats.map((stat) => {
const Icon = stat.icon;
return (
<Paper key={stat.title} p="xl" withBorder>
<Group justify="space-between">
<div>
<Text c="dimmed" fz="sm" fw={500}>
{stat.title}
</Text>
<Text fz={28} fw={700} mt={5}>
{stat.value.toLocaleString('id-ID')}
</Text>
</div>
<Badge
size="xl"
color={stat.color}
variant="light"
>
<Icon size={32} />
</Badge>
</Group>
</Paper>
);
})}
</SimpleGrid>
</Box>
{state.summary.data?.dinamika && (
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Title order={3} mb="md" c={colors["blue-button"]}>
Dinamika Penduduk
</Title>
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
<Paper p="md" bg="#6EDF9C">
<Text fz="sm" c="white">Kelahiran</Text>
<Text fz={24} fw={700} c="white">
{state.summary.data.dinamika.kelahiran}
</Text>
</Paper>
<Paper p="md" bg="#EE5050">
<Text fz="sm" c="white">Kematian</Text>
<Text fz={24} fw={700} c="white">
{state.summary.data.dinamika.kematian}
</Text>
</Paper>
<Paper p="md" bg="#5082EE">
<Text fz="sm" c="white">Pindah Masuk</Text>
<Text fz={24} fw={700} c="white">
{state.summary.data.dinamika.pindahMasuk}
</Text>
</Paper>
<Paper p="md" bg="#FF9F43">
<Text fz="sm" c="white">Pindah Keluar</Text>
<Text fz={24} fw={700} c="white">
{state.summary.data.dinamika.pindahKeluar}
</Text>
</Paper>
</SimpleGrid>
</Paper>
</Box>
)}
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,147 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, Title, Skeleton, Table, Center } from '@mantine/core';
import { IconDatabaseOff } from '@tabler/icons-react';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import dataBanjar from '@/app/admin/(dashboard)/_state/kependudukan/data-banjar';
import { useShallowEffect } from '@mantine/hooks';
function Page() {
const state = useProxy(dataBanjar)
useShallowEffect(() => {
state.findMany.load()
}, [])
const data = state.findMany.data || [];
if (state.findMany.loading) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
if (!data || data.length === 0) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Data per Banjar
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Statistik kependudukan per banjar di Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Center py="xl">
<Stack align="center" gap="md">
<IconDatabaseOff size={80} color={colors.grey['2']} />
<Text ta="center" fz="lg" fw={500} c="dimmed">
Data Belum Tersedia
</Text>
<Text ta="center" fz="sm" c="dimmed" maw={400}>
Data kependudukan per banjar untuk tahun ini belum diperbarui.
Silakan hubungi administrator desa untuk informasi lebih lanjut.
</Text>
</Stack>
</Center>
</Paper>
</Box>
</Stack>
)
}
const rows = data.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.nama}</Table.Td>
<Table.Td ta="right">{item.penduduk.toLocaleString('id-ID')}</Table.Td>
<Table.Td ta="right">{item.kk.toLocaleString('id-ID')}</Table.Td>
<Table.Td ta="right">{item.miskin.toLocaleString('id-ID')}</Table.Td>
</Table.Tr>
));
const totalPenduduk = data.reduce((sum, item) => sum + item.penduduk, 0);
const totalKK = data.reduce((sum, item) => sum + item.kk, 0);
const totalMiskin = data.reduce((sum, item) => sum + item.miskin, 0);
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Data per Banjar
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Statistik kependudukan per banjar di Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Text fw={700} fz="lg" mb="md" c="black">
Data Kependudukan per Banjar
</Text>
<Table.ScrollContainer minWidth={500}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Banjar</Table.Th>
<Table.Th ta="right">Penduduk</Table.Th>
<Table.Th ta="right">Kepala Keluarga</Table.Th>
<Table.Th ta="right">Penduduk Miskin</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows}
<Table.Tr fw={700} bg={colors.grey[1]}>
<Table.Td>Total</Table.Td>
<Table.Td ta="right">{totalPenduduk.toLocaleString('id-ID')}</Table.Td>
<Table.Td ta="right">{totalKK.toLocaleString('id-ID')}</Table.Td>
<Table.Td ta="right">{totalMiskin.toLocaleString('id-ID')}</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Table.ScrollContainer>
</Paper>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,189 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, Title, Skeleton, SimpleGrid, Center } from '@mantine/core';
import { IconDatabaseOff } from '@tabler/icons-react';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import kependudukanDashboard from '@/app/admin/(dashboard)/_state/kependudukan/dashboard';
import persentaseKelahiranKematian from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/persentaseKelahiran';
import { useShallowEffect } from '@mantine/hooks';
function Page() {
const stateKependudukan = useProxy(kependudukanDashboard);
const statePersentaseKelahiranKematian = useProxy(persentaseKelahiranKematian);
// Load migration data from kependudukan dashboard
useShallowEffect(() => {
stateKependudukan.summary.load();
}, []);
// Load birth and death data
useShallowEffect(() => {
statePersentaseKelahiranKematian.kelahiran.findMany.load();
statePersentaseKelahiranKematian.kematian.findMany.load();
}, []);
const dinamika = stateKependudukan.summary.data?.dinamika;
// Calculate birth and death counts from detailed data
const kelahiranCount = statePersentaseKelahiranKematian.kelahiran.findMany.data?.length || 0;
const kematianCount = statePersentaseKelahiranKematian.kematian.findMany.data?.length || 0;
const isLoading = stateKependudukan.summary.loading ||
statePersentaseKelahiranKematian.kelahiran.findMany.loading ||
statePersentaseKelahiranKematian.kematian.findMany.loading;
if (isLoading) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
if (!dinamika || (kelahiranCount === 0 && kematianCount === 0 && (!dinamika.pindahMasuk || dinamika.pindahMasuk === 0))) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Dinamika Penduduk
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Statistik kelahiran, kematian, dan migrasi penduduk
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Center py="xl">
<Stack align="center" gap="md">
<IconDatabaseOff size={80} color={colors.grey['2']} />
<Text ta="center" fz="lg" fw={500} c="dimmed">
Data Belum Tersedia
</Text>
<Text ta="center" fz="sm" c="dimmed" maw={400}>
Data dinamika penduduk untuk tahun ini belum diperbarui.
Silakan hubungi administrator desa untuk informasi lebih lanjut.
</Text>
</Stack>
</Center>
</Paper>
</Box>
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Dinamika Penduduk
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Statistik kelahiran, kematian, dan migrasi penduduk
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Title order={3} mb="md" c={colors["blue-button"]}>
Statistik Dinamika Penduduk
</Title>
<SimpleGrid cols={{ base: 2, md: 4 }} spacing="md">
<Paper p="lg" bg="#6EDF9C">
<Text fz="sm" c="white" fw={500}>
Kelahiran
</Text>
<Text fz={36} fw={700} c="white" mt={10}>
{kelahiranCount}
</Text>
</Paper>
<Paper p="lg" bg="#EE5050">
<Text fz="sm" c="white" fw={500}>
Kematian
</Text>
<Text fz={36} fw={700} c="white" mt={10}>
{kematianCount}
</Text>
</Paper>
<Paper p="lg" bg="#5082EE">
<Text fz="sm" c="white" fw={500}>
Pindah Masuk
</Text>
<Text fz={36} fw={700} c="white" mt={10}>
{dinamika?.pindahMasuk || 0}
</Text>
</Paper>
<Paper p="lg" bg="#FF9F43">
<Text fz="sm" c="white" fw={500}>
Pindah Keluar
</Text>
<Text fz={36} fw={700} c="white" mt={10}>
{dinamika?.pindahKeluar || 0}
</Text>
</Paper>
</SimpleGrid>
<Box mt="xl" pt="xl" style={{ borderTop: `1px solid ${colors.grey['2']}` }}>
<Text fz="md" c="dimmed" fw={500} mb="md">
Ringkasan:
</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<Paper p="md" bg={colors.grey[1]}>
<Text fz="sm" c="dimmed">Pertumbuhan Alami</Text>
<Text fz={24} fw={700} c={kelahiranCount - kematianCount >= 0 ? '#6EDF9C' : '#EE5050'}>
{kelahiranCount - kematianCount > 0 ? '+' : ''}{kelahiranCount - kematianCount}
</Text>
<Text fz="xs" c="dimmed">(Kelahiran - Kematian)</Text>
</Paper>
<Paper p="md" bg={colors.grey[1]}>
<Text fz="sm" c="dimmed">Migrasi Bersih</Text>
<Text fz={24} fw={700} c={(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0) >= 0 ? colors['blue-button'] : '#FF9F43'}>
{(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0) > 0 ? '+' : ''}{(dinamika?.pindahMasuk || 0) - (dinamika?.pindahKeluar || 0)}
</Text>
<Text fz="xs" c="dimmed">(Pindah Masuk - Pindah Keluar)</Text>
</Paper>
</SimpleGrid>
</Box>
</Paper>
</Box>
</Stack>
);
}
export default Page;

View File

@@ -0,0 +1,170 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, Title, Skeleton, Flex, ColorSwatch, Center } from '@mantine/core';
import { IconDatabaseOff } from '@tabler/icons-react';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import distribusiAgama from '@/app/admin/(dashboard)/_state/kependudukan/distribusi-agama';
import { useShallowEffect } from '@mantine/hooks';
import { PieChart } from '@mantine/charts';
function Page() {
const state = useProxy(distribusiAgama)
useShallowEffect(() => {
state.findMany.load()
}, [])
const data = state.findMany.data || [];
if (state.findMany.loading) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
if (!data || data.length === 0) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Distribusi Agama
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Komposisi agama penduduk Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Center py="xl">
<Stack align="center" gap="md">
<IconDatabaseOff size={80} color={colors.grey['2']} />
<Text ta="center" fz="lg" fw={500} c="dimmed">
Data Belum Tersedia
</Text>
<Text ta="center" fz="sm" c="dimmed" maw={400}>
Data distribusi agama untuk tahun ini belum diperbarui.
Silakan hubungi administrator desa untuk informasi lebih lanjut.
</Text>
</Stack>
</Center>
</Paper>
</Box>
</Stack>
)
}
const chartData = data.map(item => ({
name: item.agama,
value: item.jumlah,
color: getColorForAgama(item.agama),
}));
const total = data.reduce((sum, item) => sum + item.jumlah, 0);
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Distribusi Agama
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Komposisi agama penduduk Desa Darmasaba
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Text fw={700} fz="lg" mb="md" c="black">
Statistik Distribusi Agama
</Text>
<Flex direction={{ base: 'column', md: 'row' }} gap="xl" align="center">
<Box style={{ flex: 1 }}>
<PieChart
data={chartData}
size={300}
labelsPosition="inside"
withLabels
withLabelsLine
/>
</Box>
<Stack style={{ flex: 1 }} gap="md">
{data.map((item) => (
<Flex key={item.id} align="center" gap="sm">
<ColorSwatch color={getColorForAgama(item.agama)} size={20} />
<Text fz="sm" c="black" style={{ flex: 1 }}>
{item.agama}
</Text>
<Text fz="sm" fw={700} c="black">
{item.jumlah.toLocaleString('id-ID')}
</Text>
<Text fz="xs" c="dimmed">
({((item.jumlah / total) * 100).toFixed(1)}%)
</Text>
</Flex>
))}
<Box mt="md" pt="md" style={{ borderTop: `1px solid ${colors.grey['2']}` }}>
<Flex justify="space-between" align="center">
<Text fw={700} c="black">Total</Text>
<Text fw={700} c="black">{total.toLocaleString('id-ID')}</Text>
</Flex>
</Box>
</Stack>
</Flex>
</Paper>
</Box>
</Stack>
);
}
function getColorForAgama(agama: string): string {
const colors: Record<string, string> = {
'HINDU': '#FF9F43',
'ISLAM': '#6EDF9C',
'KRISTEN': '#5082EE',
'KRISTEN_PROTESTAN': '#5082EE',
'KRISTEN_KATOLIK': '#4263D1',
'BUDDHA': '#FFD43B',
'KONGHUCU': '#EE5050',
'LAINNYA': '#868E96',
};
return colors[agama] || '#868E96';
}
export default Page;

View File

@@ -0,0 +1,150 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Paper, Text, Title, Skeleton, Center } from '@mantine/core';
import { IconDatabaseOff } from '@tabler/icons-react';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { useProxy } from 'valtio/utils';
import distribusiUmur from '@/app/admin/(dashboard)/_state/kependudukan/distribusi-umur';
import { useShallowEffect } from '@mantine/hooks';
import { BarChart } from '@mantine/charts';
function Page() {
const state = useProxy(distribusiUmur)
useShallowEffect(() => {
state.findMany.load()
}, [])
const data = state.findMany.data || [];
if (state.findMany.loading) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
if (!data || data.length === 0) {
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Distribusi Umur
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Komposisi penduduk berdasarkan kelompok umur
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Center py="xl">
<Stack align="center" gap="md">
<IconDatabaseOff size={80} color={colors.grey['2']} />
<Text ta="center" fz="lg" fw={500} c="dimmed">
Data Belum Tersedia
</Text>
<Text ta="center" fz="sm" c="dimmed" maw={400}>
Data distribusi umur untuk tahun ini belum diperbarui.
Silakan hubungi administrator desa untuk informasi lebih lanjut.
</Text>
</Stack>
</Center>
</Paper>
</Box>
</Stack>
)
}
// Sort data by age range (extract the first number from rentangUmur)
const sortedData = [...data].sort((a, b) => {
const extractMinAge = (range: string) => {
const match = range.match(/^(\d+)/);
return match ? parseInt(match[1], 10) : 999;
};
return extractMinAge(a.rentangUmur) - extractMinAge(b.rentangUmur);
});
const chartData = sortedData.map(item => ({
umur: item.rentangUmur,
jumlah: item.jumlah,
}));
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"lg"}>
<Box px={{ base: 'md', md: 100 }}>
<BackButton />
</Box>
<Box px={{ base: 'md', md: 100 }}>
<Title
order={1}
ta="center"
c={colors["blue-button"]}
fw="bold"
lh={1.2}
>
Distribusi Umur
</Title>
<Text
ta="center"
fz={{ base: 'sm', md: 'md' }}
lh={1.5}
c="black"
>
Komposisi penduduk berdasarkan kelompok umur
</Text>
</Box>
<Box px={{ base: "md", md: 100 }}>
<Paper p="xl" withBorder>
<Text fw={700} fz="lg" mb="md" c="black">
Statistik Distribusi Umur
</Text>
<BarChart
h={400}
data={chartData}
dataKey="umur"
series={[{ name: 'jumlah', color: colors['blue-button'] }]}
tickLine="y"
yAxisProps={{
width: 80,
}}
xAxisProps={{
angle: -45,
textAnchor: 'end',
height: 100,
interval: 0,
style: {
fontSize: '12px',
}
}}
gridAxis="y"
withXAxis
withYAxis
/>
</Paper>
</Box>
</Stack>
);
}
export default Page;