Compare commits

...

3 Commits

Author SHA1 Message Date
7b2b306849 API & UI Menu Ekonomi, Submenu Jumlah Pengangguran 2025-07-10 00:21:33 +08:00
d328f64d86 UI & API Tabs Detail Data Pengangguran 2025-07-09 15:25:48 +08:00
119275b95c API & State Jumlah Pengangguran 2025-07-09 12:00:37 +08:00
46 changed files with 1466 additions and 269 deletions

View File

@@ -0,0 +1,51 @@
[
{
"month": "Jan",
"year": 2025,
"totalUnemployment": 160,
"educatedUnemployment": 95,
"uneducatedUnemployment": 65,
"percentageChange": null
},
{
"month": "Feb",
"year": 2025,
"totalUnemployment": 155,
"educatedUnemployment": 90,
"uneducatedUnemployment": 65,
"percentageChange": -3.1
},
{
"month": "Mar",
"year": 2025,
"totalUnemployment": 150,
"educatedUnemployment": 88,
"uneducatedUnemployment": 62,
"percentageChange": -3.2
},
{
"month": "Apr",
"year": 2025,
"totalUnemployment": 148,
"educatedUnemployment": 85,
"uneducatedUnemployment": 63,
"percentageChange": -1.3
},
{
"month": "Mei",
"year": 2025,
"totalUnemployment": 145,
"educatedUnemployment": 82,
"uneducatedUnemployment": 63,
"percentageChange": -2.0
},
{
"month": "Jun",
"year": 2025,
"totalUnemployment": 140,
"educatedUnemployment": 80,
"uneducatedUnemployment": 60,
"percentageChange": -3.4
}
]

View File

@@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "DetailDataPengangguran" (
"id" UUID NOT NULL,
"month" VARCHAR(20) NOT NULL,
"year" INTEGER NOT NULL,
"totalUnemployment" INTEGER NOT NULL,
"educatedUnemployment" INTEGER NOT NULL,
"uneducatedUnemployment" INTEGER NOT NULL,
"percentageChange" DOUBLE PRECISION,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "DetailDataPengangguran_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "DetailDataPengangguran_month_year_key" ON "DetailDataPengangguran"("month", "year");

View File

@@ -1262,3 +1262,20 @@ model DataDemografiPekerjaan {
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= JUMLAH PENGANGGURAN ========================================= //
model DetailDataPengangguran {
id String @id @default(uuid()) @db.Uuid
month String @db.VarChar(20)
year Int
totalUnemployment Int
educatedUnemployment Int
uneducatedUnemployment Int
percentageChange Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
@@unique([month, year])
}

View File

@@ -21,6 +21,7 @@ import kategoriProduk from "./data/ekonomi/pasar-desa/kategori-produk.json";
import hubunganOrganisasi from "./data/ekonomi/struktur-organisasi/hubungan-organisasi.json";
import posisiOrganisasi from "./data/ekonomi/struktur-organisasi/posisi-organisasi.json";
import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
import detailDataPengangguran from './data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json';
(async () => {
for (const l of layanan) {
@@ -431,6 +432,29 @@ import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
});
}
console.log("hubungan organisasi success ...");
for (const d of detailDataPengangguran) {
await prisma.detailDataPengangguran.upsert({
where: {
month_year: { month: d.month, year: d.year },
},
update: {
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
create: {
month: d.month,
year: d.year,
totalUnemployment: d.totalUnemployment,
educatedUnemployment: d.educatedUnemployment,
uneducatedUnemployment: d.uneducatedUnemployment,
percentageChange: d.percentageChange,
},
});
}
console.log("📊 detailDataPengangguran success ...");
})()
.then(() => prisma.$disconnect())
.catch((e) => {

View File

@@ -15,6 +15,8 @@ type JudulListTabProps = {
}
const JudulListTab = ({
title = "",
href = "#",

View File

@@ -0,0 +1,243 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateJumlahPengngguran = z.object({
month: z.string().min(1, "Bulan harus diisi"),
year: z.number().min(1, "Tahun harus diisi"),
totalUnemployment: z.number().min(1, "Total pengangguran harus diisi"),
educatedUnemployment: z
.number()
.min(1, "Pengangguran pendidikan harus diisi"),
uneducatedUnemployment: z
.number()
.min(1, "Pengangguran tidak pendidikan harus diisi"),
percentageChange: z.number().min(0, "Persentase perubahan harus diisi"),
});
type JumlahPengangguran = {
month: string;
year: number;
totalUnemployment: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
};
const jumlahPengangguranForm: JumlahPengangguran = {
month: "",
year: 0,
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
percentageChange: 0,
};
const jumlahPengangguran = proxy({
findByMonthYear: {
data: null as any,
loading: false,
load: async ({ month, year }: { month: string; year: number }) => {
jumlahPengangguran.findByMonthYear.loading = true;
try {
const res = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/month/${month}/year/${year}`
);
const json = await res.json();
jumlahPengangguran.findByMonthYear.data = json.data;
return json.data;
} catch (err) {
console.error("Gagal ambil data bulan/tahun:", err);
} finally {
jumlahPengangguran.findByMonthYear.loading = false;
}
},
},
create: {
form: jumlahPengangguranForm,
loading: false,
async create() {
const cek = templateJumlahPengngguran.safeParse(
jumlahPengangguran.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return null;
}
try {
jumlahPengangguran.create.loading = true;
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"create"
].post(jumlahPengangguran.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPengangguran.create.form = { ...jumlahPengangguranForm };
jumlahPengangguran.findMany.load();
return id;
}
}
toast.error("failed create");
return null;
} catch (error) {
console.log((error as Error).message);
return null;
} finally {
jumlahPengangguran.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DetailDataPengangguranGetPayload<{
omit: { isActive: true };
}>[]
| null,
async load() {
const res =
await ApiFetch.api.ekonomi.jumlahpengangguran.detaildatapengangguran[
"find-many"
].get();
if (res.status === 200) {
jumlahPengangguran.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DetailDataPengangguranGetPayload<{
omit: { isActive: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/${id}`
);
if (res.ok) {
const data = await res.json();
jumlahPengangguran.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch jumlahPengangguran:", res.statusText);
jumlahPengangguran.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching jumlahPengangguran:", error);
jumlahPengangguran.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...jumlahPengangguranForm },
loading: false,
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const formData = {
month: this.form.month,
year: this.form.year,
totalUnemployment: this.form.totalUnemployment,
educatedUnemployment: this.form.educatedUnemployment,
uneducatedUnemployment: this.form.uneducatedUnemployment,
percentageChange: this.form.percentageChange,
};
const cek = templateJumlahPengngguran.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/ekonomi/jumlahpengangguran/detaildatapengangguran/${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 jumlahPengangguran.findMany.load();
return result.data;
} catch (error) {
console.error("Update error:", error);
toast.error("Gagal update data jumlah pengangguran");
throw error;
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPengangguran.delete.loading = true;
const response = await fetch(
`/api/ekonomi/jumlahpengangguran/detaildatapengangguran/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Jumlah pengangguran berhasil dihapus"
);
await jumlahPengangguran.findMany.load();
} else {
toast.error(result?.message || "Gagal menghapus jumlah pengangguran");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus jumlah pengangguran");
} finally {
jumlahPengangguran.delete.loading = false;
}
},
},
});
const jumlahPengangguranState = proxy({
jumlahPengangguran,
});
export default jumlahPengangguranState;

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { useProxy } from 'valtio/utils';
import stateGallery from '../../../_state/desa/gallery';
import { useShallowEffect } from '@mantine/hooks';

View File

@@ -1,5 +1,5 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Image, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';

View File

@@ -1,5 +1,5 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';

View File

@@ -6,7 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../_com/jusulListTab';
import JudulListTab from '../../_com/judulListTab';
import { useState } from 'react';
import HeaderSearch from '../../_com/header';

View File

@@ -1,11 +1,11 @@
'use client'
import colors from '@/con/colors';
import { BarChart } from '@mantine/charts';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
@@ -40,8 +40,6 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
const stateDemografi = useProxy(demografiPekerjaan)
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
@@ -54,7 +52,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
stateDemografi.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateDemografi.findMany.load()
@@ -104,7 +102,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/kesehatan/data-kesehatan-warga/persentase_data_kelahiran_kematian/${item.id}`)}>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
@@ -138,14 +136,18 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="pekerjaan" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="lakiLaki" fill="#f03e3e" name="Laki - Laki" />
<Bar dataKey="perempuan" fill="#ff922b" name="Perempuan" />
</BarChart>
<Box w={{ base: '100%', md: '30%' }}>
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: 'red.6', label: 'Laki - Laki' },
{ name: 'perempuan', color: 'orange.6', label: 'Perempuan' },
]}
/>
</Box>
)}
</Paper>
</Box>

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';

View File

@@ -1,45 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function CreateJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jumlah Pengangguran</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Terdidik</Text>}
placeholder='Masukkan pengangguran terdidik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia Produktif</Text>}
placeholder='Masukkan usia produktif'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Sedang Mencari Kerja</Text>}
placeholder='Masukkan sedang mencari kerja'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Tidak Terdidik</Text>}
placeholder='Masukkan pengangguran tidak terdidik'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPengangguran;

View File

@@ -1,66 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Stack, Text } from '@mantine/core';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
function DetailJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Jumlah Pengangguran</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text>
<Text>100</Text>
</Box>
<Box>
<Text fw={"bold"}>Usia Produktif</Text>
<Text>200</Text>
</Box>
<Box>
<Text fw={"bold"}>Sedang Mencari Kerja</Text>
<Text>300</Text>
</Box>
<Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text>
<Text>30</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button color="red">
<IconX size={20} />
</Button>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran-2024-2025/edit')} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus potensi ini?"
/> */}
</Box>
);
}
export default DetailJumlahPengangguran;

View File

@@ -1,45 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
function EditJumlahPengangguran() {
const router = useRouter();
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{base: '100%', md: '50%'}} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jumlah Pengangguran</Title>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Terdidik</Text>}
placeholder='Masukkan pengangguran terdidik'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Usia Produktif</Text>}
placeholder='Masukkan usia produktif'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Sedang Mencari Kerja</Text>}
placeholder='Masukkan sedang mencari kerja'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Pengangguran Tidak Terdidik</Text>}
placeholder='Masukkan pengangguran tidak terdidik'
/>
<Group>
<Button bg={colors['blue-button']}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditJumlahPengangguran;

View File

@@ -1,58 +0,0 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
function JumlahPengangguran() {
return (
<Box>
<HeaderSearch
title='Jumlah Pengangguran 2024-2025'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListJumlahPengangguran/>
</Box>
);
}
function ListJumlahPengangguran() {
const router = useRouter();
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Jumlah Pengangguran 2024-2025'
href='/admin/ekonomi/jumlah-pengangguran-2024-2025/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pengangguran Terdidik</TableTh>
<TableTh>Usia Produktif</TableTh>
<TableTh>Sedang Mencari Kerja</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
<TableTr>
<TableTd>100</TableTd>
<TableTd>200</TableTd>
<TableTd>300</TableTd>
<TableTd>
<Button onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran-2024-2025/detail')}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default JumlahPengangguran;

View File

@@ -0,0 +1,185 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter();
const params = useParams()
const [formData, setFormData] = useState({
month: stateDetail.update.form.month,
year: stateDetail.update.form.year,
totalUnemployment: stateDetail.update.form.totalUnemployment,
educatedUnemployment: stateDetail.update.form.educatedUnemployment,
uneducatedUnemployment: stateDetail.update.form.uneducatedUnemployment,
percentageChange: stateDetail.update.form.percentageChange || 0, // Ensure it's always a number
})
const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// Ambil data bulan sebelumnya
const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === formData.month.toLowerCase()
);
let percentageChange = 0;
if (currentIndex > 0) {
const prevMonth = monthOrder[currentIndex - 1];
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: formData.year,
});
if (prev?.totalUnemployment) {
percentageChange = Number(
(((total - prev.totalUnemployment) / prev.totalUnemployment) * 100).toFixed(1)
);
}
}
setFormData({
...formData,
totalUnemployment: total,
percentageChange,
});
return { total, percentageChange };
};
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateDetail.findUnique.load(id); // ambil by ID
const data = stateDetail.findUnique.data;
if (data) {
// Set the ID for update
stateDetail.update.id = id;
// Isi state Valtio untuk update
stateDetail.update.form = {
...data,
percentageChange: data.percentageChange || 0 // Ensure it's always a number
};
// Isi local formData supaya input bisa dikontrol
setFormData({
month: data.month,
year: data.year,
totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
percentageChange: data.percentageChange || 0, // Ensure it's always a number
});
}
} catch (error) {
console.error("Error loading detail:", error);
toast.error("Gagal memuat data detail");
}
};
loadDetail();
}, [params?.id]);
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange();
try {
stateDetail.update.form = {
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit();
if (success) {
toast.success("Detail data pengangguran berhasil diperbarui!");
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran");
}
} catch (error) {
console.error("Error updating:", error);
toast.error("Terjadi kesalahan saat memperbarui data");
}
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Edit Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
label="Bulan"
value={formData.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(val) => (setFormData({
...formData,
month: val.currentTarget.value
}))}
/>
<TextInput
label="Tahun"
type="number"
value={formData.year}
onChange={(val) => (setFormData({
...formData,
year: Number(val.currentTarget.value)
}))}
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
value={formData.educatedUnemployment}
onChange={(val) => (setFormData({
...formData,
educatedUnemployment: Number(val.currentTarget.value)
}))}
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
value={formData.uneducatedUnemployment}
onChange={(val) => (setFormData({
...formData,
uneducatedUnemployment: Number(val.currentTarget.value)
}))}
/>
<Text fz="sm" fw={500}>
Total Otomatis: {formData.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{formData.percentageChange !== null
? `${formData.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditDetailDataPengangguran;

View File

@@ -0,0 +1,111 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailJumlahPengangguran() {
const router = useRouter();
const params = useParams()
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string)
}, [params?.id])
const handleHapus = () => {
if (selectedId) {
stateDetail.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran")
}
}
if (!stateDetail.findUnique.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}>
<Stack>
<Text fz={"xl"} fw={"bold"}>Detail Data Pengangguran</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text>
<Text>{stateDetail.findUnique.data?.educatedUnemployment}</Text>
</Box>
<Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text>
<Text>{stateDetail.findUnique.data?.uneducatedUnemployment}</Text>
</Box>
<Box>
<Text fw={"bold"}>Perubahan</Text>
<Text>{stateDetail.findUnique.data?.percentageChange}</Text>
</Box>
<Box>
<Text fw={"bold"}>Tahun</Text>
<Text>{stateDetail.findUnique.data?.year}</Text>
</Box>
<Box>
<Text fw={"bold"}>Bulan</Text>
<Text>{stateDetail.findUnique.data?.month}</Text>
</Box>
<Box>
<Text fw={"bold"}>Total Pengangguran</Text>
<Text>{stateDetail.findUnique.data?.totalUnemployment}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (stateDetail.findUnique.data) {
setSelectedId(stateDetail.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data}
color={"red"}>
<IconX size={20} />
</Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green">
<IconEdit size={20} />
</Button>
</Flex>
</Box>
</Stack>
</Paper>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?"
/>
</Box>
);
}
export default DetailJumlahPengangguran;

View File

@@ -0,0 +1,139 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter();
const resetForm = () => {
stateDetail.create.form = {
month: "",
year: 0,
totalUnemployment: 0,
educatedUnemployment: 0,
uneducatedUnemployment: 0,
percentageChange: 0,
}
}
const calculateTotalAndChange = async () => {
const total =
stateDetail.create.form.educatedUnemployment +
stateDetail.create.form.uneducatedUnemployment;
stateDetail.create.form.totalUnemployment = total;
// Ambil data bulan sebelumnya
const monthOrder = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
);
if (currentIndex > 0) {
const prevMonth = monthOrder[currentIndex - 1];
const prev = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: stateDetail.create.form.year,
});
if (prev?.totalUnemployment) {
const change = ((total - prev.totalUnemployment) / prev.totalUnemployment) * 100;
stateDetail.create.form.percentageChange = Number(change.toFixed(1));
} else {
stateDetail.create.form.percentageChange = 0;
}
} else {
stateDetail.create.form.percentageChange = 0;
}
};
const handleSubmit = async () => {
await calculateTotalAndChange();
const id = await stateDetail.create.create();
if (id) {
await stateDetail.findUnique.load(String(id));
if (stateDetail.findUnique.data) {
setChartData([stateDetail.findUnique.data]);
}
resetForm();
router.push('/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran');
}
};
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25}/>
</Button>
</Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Detail Data Pengangguran</Title>
<Stack gap="xs">
<TextInput
label="Bulan"
value={stateDetail.create.form.month}
placeholder="Contoh: Jan, Feb, Mar"
onChange={(e) => (stateDetail.create.form.month = e.currentTarget.value)}
/>
<TextInput
label="Tahun"
type="number"
value={stateDetail.create.form.year}
onChange={(e) =>
(stateDetail.create.form.year = Number(e.currentTarget.value))
}
/>
<TextInput
label="Pengangguran Terdidik"
type="number"
value={stateDetail.create.form.educatedUnemployment}
onChange={(e) => {
stateDetail.create.form.educatedUnemployment = Number(
e.currentTarget.value,
);
}}
/>
<TextInput
label="Pengangguran Tidak Terdidik"
type="number"
value={stateDetail.create.form.uneducatedUnemployment}
onChange={(e) => {
stateDetail.create.form.uneducatedUnemployment = Number(
e.currentTarget.value,
);
}}
/>
<Text fz="sm" fw={500}>
Total Otomatis: {stateDetail.create.form.totalUnemployment}
</Text>
<Text fz="sm" fw={500}>
Perubahan Otomatis:{" "}
{stateDetail.create.form.percentageChange !== null
? `${stateDetail.create.form.percentageChange}%`
: '-'}
</Text>
<Group>
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateJumlahPengangguran;

View File

@@ -0,0 +1,148 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
import JudulListTab from '../../_com/judulListTab';
function DetailDataPengangguran() {
return (
<Box>
<Stack gap={"xs"}>
<Title order={3}>Detail Data Pengangguran</Title>
<ListDetailDataPengangguran />
</Stack>
</Box>
);
}
function ListDetailDataPengangguran() {
type DetailDataPengangguran = {
id: string;
month: string;
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
totalUnemployment: number;
}
const [chartData, setChartData] = useState<DetailDataPengangguran[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter();
const [search, setSearch] = useState("")
useShallowEffect(() => {
setMounted(true)
stateDetail.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateDetail.findMany.data) {
setChartData(stateDetail.findMany.data.map((item) => ({
id: item.id,
month: item.month,
year: item.year,
educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment),
})));
}
}, [stateDetail.findMany.data]);
const filteredData = (stateDetail.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.month.toLowerCase().includes(keyword) ||
item.year.toString().toLowerCase().includes(keyword)
);
});
if (!stateDetail.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
}
return (
<Box>
<Stack gap={"md"}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulListTab
title='List Detail Data Pengangguran'
href='/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/create'
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder='pencarian'
searchIcon={<IconSearch size={16} />}
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Bulan</TableTh>
<TableTh>Terdidik</TableTh>
<TableTh>Tidak Terdidik</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.month}</TableTd>
<TableTd>{item.educatedUnemployment}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/detail-data-pengangguran/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box>
) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
)}
</Paper>
</Box>
)}
</Stack>
</Box>
);
}
export default DetailDataPengangguran;

View File

@@ -1,14 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Badge, Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImacCog, IconSearch } 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 JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
function Pegawai() {
const [search, setSearch] = useState("");
@@ -30,7 +30,7 @@ function ListPegawai({ search }: { search: string }) {
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const router = useRouter();
useShallowEffect(() => {
useEffect(() => {
const loadData = async () => {
try {
// Clear existing data to ensure we see the loading state

View File

@@ -1,15 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconEdit, 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 JudulList from '../../../_com/judulList';
import { useProxy } from 'valtio/utils';
import { useShallowEffect } from '@mantine/hooks';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
import { useState } from 'react';
function PosisiOrganisasi() {
const [search, setSearch] = useState("");
@@ -33,7 +33,7 @@ function ListPosisiOrganisasi({ search }: { search: string }) {
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
useShallowEffect(() => {
useEffect(() => {
stateOrganisasi.findMany.load()
}, [])

View File

@@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikkepuasan from '../../../_state/kesehatan/data_kesehatan_warga/grafikKepuasan';
import HeaderSearch from '../../../_com/header';

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
function KeteranganBankSampahTerdekat() {
const router = useRouter();

View File

@@ -3,7 +3,7 @@ import colors from '@/con/colors';
import { Box, Button, Image, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
function ListPengelolaanSampahBankSampah() {
const router = useRouter();

View File

@@ -9,7 +9,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';

View File

@@ -8,7 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useSnapshot } from 'valtio';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikBerdasarkanResponden from '../../../_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
import HeaderSearch from '../../../_com/header';

View File

@@ -9,7 +9,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import JudulListTab from '../../../_com/jusulListTab';
import JudulListTab from '../../../_com/judulListTab';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import HeaderSearch from '../../../_com/header';

View File

@@ -1,5 +1,5 @@
'use client'
import JudulListTab from '@/app/admin/(dashboard)/_com/jusulListTab';
import JudulListTab from '@/app/admin/(dashboard)/_com/judulListTab';
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';

View File

@@ -234,8 +234,8 @@ export const navBar = [
},
{
id: "Ekonomi_5",
name: "Jumlah Pengangguran 2024-2025",
path: "/admin/ekonomi/jumlah-pengangguran-2024-2025"
name: "Jumlah Pengangguran",
path: "/admin/ekonomi/jumlah-pengangguran"
},
{
id: "Ekonomi_6",
@@ -244,8 +244,8 @@ export const navBar = [
},
{
id: "Ekonomi_7",
name: "Jumlah Penduduk Miskin 2024-2025",
path: "/admin/ekonomi/jumlah-penduduk-miskin-2024-2025"
name: "Jumlah Penduduk Miskin",
path: "/admin/ekonomi/jumlah-penduduk-miskin"
},
{
id: "Ekonomi_8",

View File

@@ -9,6 +9,7 @@ import GrafikMenganggurBerdasarkanPendidikan from "./usia-kerja-yang-menganggur/
import JumlahPendudukMiskin from "./jumlah-penduduk-miskin";
import SektorUnggulanDesa from "./sektor-unggulan-desa";
import DemografiPekerjaan from "./demografi-pekerjaan";
import JumlahPengangguran from "./jumlah-pengangguran";
const Ekonomi = new Elysia({
prefix: "/api/ekonomi",
@@ -24,5 +25,6 @@ const Ekonomi = new Elysia({
.use(JumlahPendudukMiskin)
.use(SektorUnggulanDesa)
.use(DemografiPekerjaan)
.use(JumlahPengangguran)
export default Ekonomi

View File

@@ -0,0 +1,139 @@
// import prisma from "@/lib/prisma";
// import { Prisma } from "@prisma/client";
// import { Context } from "elysia";
// type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
// select: {
// month: true;
// year: true;
// totalUnemployment: true;
// educatedUnemployment: true;
// uneducatedUnemployment: true;
// percentageChange: true;
// };
// }>;
// export default async function detailDataPengangguranCreate(context: Context) {
// const body = context.body as FormCreate;
// // Cek apakah data untuk bulan & tahun tersebut sudah ada
// const existing = await prisma.detailDataPengangguran.findFirst({
// where: {
// month: body.month,
// year: body.year,
// },
// });
// if (existing) {
// return {
// success: false,
// message: `Data bulan ${body.month} ${body.year} sudah ada.`,
// data: null,
// };
// }
// const created = await prisma.detailDataPengangguran.create({
// data: {
// month: body.month,
// year: body.year,
// totalUnemployment: body.totalUnemployment,
// educatedUnemployment: body.educatedUnemployment,
// uneducatedUnemployment: body.uneducatedUnemployment,
// percentageChange: body.percentageChange ?? null,
// },
// select: {
// id: true,
// month: true,
// year: true,
// totalUnemployment: true,
// educatedUnemployment: true,
// uneducatedUnemployment: true,
// percentageChange: true,
// },
// });
// return {
// success: true,
// message: "Berhasil menambahkan data pengangguran bulanan",
// data: created,
// };
// }
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DetailDataPengangguranGetPayload<{
select: {
month: true;
year: true;
totalUnemployment: true;
educatedUnemployment: true;
uneducatedUnemployment: true;
};
}>;
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
export default async function detailDataPengangguranCreate(context: Context) {
const body = context.body as FormCreate;
const existing = await prisma.detailDataPengangguran.findFirst({
where: {
month: body.month,
year: body.year,
},
});
if (existing) {
return {
success: false,
message: `Data bulan ${body.month} ${body.year} sudah ada.`,
data: null,
};
}
// Cari bulan sebelumnya
const currentMonthIndex = monthOrder.indexOf(body.month);
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
const prevYear = currentMonthIndex > 0 ? body.year : body.year - 1;
const prevData = await prisma.detailDataPengangguran.findFirst({
where: {
month: prevMonth,
year: prevYear,
},
});
let percentageChange: number | null = null;
if (prevData) {
const change = ((body.totalUnemployment - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
const created = await prisma.detailDataPengangguran.create({
data: {
month: body.month,
year: body.year,
totalUnemployment: body.totalUnemployment,
educatedUnemployment: body.educatedUnemployment,
uneducatedUnemployment: body.uneducatedUnemployment,
percentageChange,
},
select: {
id: true,
month: true,
year: true,
totalUnemployment: true,
educatedUnemployment: true,
uneducatedUnemployment: true,
percentageChange: true,
},
});
return {
success: true,
message: "Berhasil menambahkan data pengangguran bulanan",
data: created,
};
}

View File

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

View File

@@ -0,0 +1,35 @@
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function findByMonthYear(context: Context) {
const { month, year } = context.params as { month: string; year: string };
if (!month || !year) {
return {
success: false,
message: "Bulan dan tahun wajib diisi",
data: null,
};
}
const data = await prisma.detailDataPengangguran.findFirst({
where: {
month,
year: Number(year),
},
});
if (!data) {
return {
success: false,
message: `Data untuk bulan ${month} tahun ${year} tidak ditemukan`,
data: null,
};
}
return {
success: true,
message: "Data ditemukan",
data,
};
}

View File

@@ -0,0 +1,10 @@
import prisma from "@/lib/prisma";
export default async function detailDataPengangguranFindMany() {
const res = await prisma.detailDataPengangguran.findMany({
orderBy: [{ year: "desc" }, { month: "asc" }],
});
return {
data: res,
};
}

View File

@@ -0,0 +1,46 @@
import prisma from "@/lib/prisma";
export default async function detailDataPengangguranFindUnique(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.detailDataPengangguran.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,55 @@
import Elysia, { t } from "elysia";
import detailDataPengangguranCreate from "./create";
import detailDataPengangguranDelete from "./del";
import findByMonthYear from "./findByMonthYear";
import detailDataPengangguranFindMany from "./findMany";
import detailDataPengangguranFindUnique from "./findUnique";
import detailDataPengangguranUpdate from "./updt";
const DetailDataPengangguran = new Elysia({
prefix: "/detaildatapengangguran",
tags: ["Ekonomi/Jumlah Pengangguran/Detail Data Pengangguran"],
})
.get("/:id", async (context) => {
const response = await detailDataPengangguranFindUnique(
new Request(context.request)
);
return response;
})
.get("/find-many", detailDataPengangguranFindMany)
.post("/create", detailDataPengangguranCreate, {
body: t.Object({
month: t.String(),
year: t.Number(),
totalUnemployment: t.Number(),
educatedUnemployment: t.Number(),
uneducatedUnemployment: t.Number(),
percentageChange: t.Number(),
}),
})
.put("/:id", detailDataPengangguranUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
month: t.String(),
year: t.Number(),
totalUnemployment: t.Number(),
educatedUnemployment: t.Number(),
uneducatedUnemployment: t.Number(),
percentageChange: t.Number(),
}),
})
.delete("/del/:id", detailDataPengangguranDelete, {
params: t.Object({
id: t.String(),
}),
})
.get("/month/:month/year/:year", findByMonthYear, {
params: t.Object({
month: t.String(),
year: t.String(),
}),
})
export default DetailDataPengangguran;

View File

@@ -0,0 +1,145 @@
// import prisma from "@/lib/prisma";
// import { Context } from "elysia";
// export default async function detailDataPengangguranUpdate(context: Context) {
// const id = context.params?.id;
// if (!id) {
// return {
// success: false,
// message: "ID tidak ditemukan",
// }
// }
// const { month, year, totalUnemployment, educatedUnemployment, uneducatedUnemployment, percentageChange } = context.body as {
// month: string;
// year: number;
// totalUnemployment: number;
// educatedUnemployment: number;
// uneducatedUnemployment: number;
// percentageChange: number;
// }
// const duplicate = await prisma.detailDataPengangguran.findFirst({
// where: {
// month,
// year,
// NOT: { id },
// },
// });
// if (duplicate) {
// return {
// success: false,
// message: `Data bulan ${month} ${year} sudah ada.`,
// };
// }
// const existing = await prisma.detailDataPengangguran.findUnique({
// where: {
// id: id,
// },
// })
// if (!existing) {
// return {
// success: false,
// message: "Data tidak ditemukan",
// }
// }
// const updated = await prisma.detailDataPengangguran.update({
// where: { id },
// data: {
// month,
// year,
// totalUnemployment,
// educatedUnemployment,
// uneducatedUnemployment,
// percentageChange,
// },
// })
// return {
// success: true,
// message: "Data berhasil diupdate",
// data: updated,
// }
// }
import prisma from "@/lib/prisma";
import { Context } from "elysia";
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"];
export default async function detailDataPengangguranUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return { success: false, message: "ID tidak ditemukan" };
}
const { month, year, totalUnemployment, educatedUnemployment, uneducatedUnemployment } = context.body as {
month: string;
year: number;
totalUnemployment: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
};
const duplicate = await prisma.detailDataPengangguran.findFirst({
where: {
month,
year,
NOT: { id },
},
});
if (duplicate) {
return { success: false, message: `Data bulan ${month} ${year} sudah ada.` };
}
const current = await prisma.detailDataPengangguran.findUnique({ where: { id } });
if (!current) {
return { success: false, message: "Data tidak ditemukan" };
}
const currentMonthIndex = monthOrder.indexOf(month);
const prevMonth = currentMonthIndex > 0 ? monthOrder[currentMonthIndex - 1] : "Des";
const prevYear = currentMonthIndex > 0 ? year : year - 1;
const prevData = await prisma.detailDataPengangguran.findFirst({
where: {
month: prevMonth,
year: prevYear,
},
});
let percentageChange: number | null = null;
if (prevData) {
const change = ((totalUnemployment - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
const updated = await prisma.detailDataPengangguran.update({
where: { id },
data: {
month,
year,
totalUnemployment,
educatedUnemployment,
uneducatedUnemployment,
percentageChange,
},
});
return {
success: true,
message: "Data berhasil diupdate",
data: updated,
};
}

View File

@@ -0,0 +1,10 @@
import Elysia from "elysia";
import DetailDataPengangguran from "./detail-data-pengangguran";
const JumlahPengangguran = new Elysia({
prefix: "/jumlahpengangguran",
})
.use(DetailDataPengangguran)
export default JumlahPengangguran;

View File

@@ -1,9 +1,8 @@
import colors from '@/con/colors';
import { Stack, Box, Text, Group, Flex, Button, SimpleGrid, Paper, Center, ColorSwatch, TableTd, TableTr, Table, TableTbody, TableTh, TableThead } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { IconBriefcase, IconChevronDown, IconDownload, IconSchool, IconUserOff, IconUsersGroup } from '@tabler/icons-react';
import { BarChart } from '@mantine/charts';
import { Box, Button, Center, ColorSwatch, Flex, Group, Paper, SimpleGrid, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
import { IconChevronDown, IconDownload, IconSchool, IconSchoolOff, IconUserOff } from '@tabler/icons-react';
import BackButton from '../../desa/layanan/_com/BackButto';
const data1 = [
{
@@ -22,17 +21,10 @@ const data1 = [
},
{
id: 3,
icon: <IconUsersGroup size={35} />,
judul: 'Usia Produktif',
jumlah: '125',
persentase: <Text fz={'h4'} c={'gray'}>89.3% dari total</Text>
},
{
id: 4,
icon: <IconBriefcase size={35} />,
judul: 'Sedang Mencari Kerja',
jumlah: '95',
persentase: <Text fz={'h4'} c={'red'}>67.9% dari total</Text>
icon: <IconSchoolOff size={35} />,
judul: 'Pengangguran Tidak Terdidik',
jumlah: '60',
persentase: <Text fz={'h4'} c={'gray'}>42.9% dari total</Text>
},
]