Fix Menu Ekonomi :

Pasar Desa : Kategorinya ga tampil,
Bug inputan edit di submenu : Demografi pekerjaa
This commit is contained in:
2025-10-04 21:34:31 +08:00
parent f7fd9be255
commit 5c66eccf23
22 changed files with 648 additions and 457 deletions

View File

@@ -13,6 +13,7 @@ const templateForm = z.object({
gaji: z.string(),
deskripsi: z.string(),
kualifikasi: z.string(),
notelp: z.string(),
});
const defaultForm = {
@@ -23,6 +24,7 @@ const defaultForm = {
gaji: "",
deskripsi: "",
kualifikasi: "",
notelp: "",
};
const lowonganKerjaState = proxy({
@@ -179,6 +181,7 @@ const lowonganKerjaState = proxy({
gaji: data.gaji,
deskripsi: data.deskripsi,
kualifikasi: data.kualifikasi,
notelp: data.notelp,
};
return data;
} else {
@@ -218,6 +221,7 @@ const lowonganKerjaState = proxy({
gaji: this.form.gaji,
deskripsi: this.form.deskripsi,
kualifikasi: this.form.kualifikasi,
notelp: this.form.notelp,
}),
});
if (!response.ok) {

View File

@@ -14,7 +14,7 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
@@ -25,59 +25,65 @@ interface FormData {
perempuan: number;
}
function EditDemografiPekerjaan() {
export default function EditDemografiPekerjaan() {
const router = useRouter();
const params = useParams() as { id: string };
const { id } = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id;
const [formData, setFormData] = useState<FormData>({
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
});
// Load data sekali waktu
// Load data hanya sekali di awal (tidak reset form)
useEffect(() => {
if (!id) return;
stateDemografi.update.id = id;
stateDemografi.findUnique
.load(id)
.then(() => {
const loadData = async () => {
try {
stateDemografi.update.id = id;
await stateDemografi.findUnique.load(id);
const data = stateDemografi.findUnique.data;
if (data) {
setFormData({
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0),
pekerjaan: data.pekerjaan ?? '',
lakiLaki: Number(data.lakiLaki ?? 0),
perempuan: Number(data.perempuan ?? 0),
});
}
})
.catch((error) => {
} catch (error) {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}, [id]);
const handleChange =
(field: keyof FormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[field]:
field === 'lakiLaki' || field === 'perempuan'
? Number(e.currentTarget.value)
: e.currentTarget.value,
}));
}
};
loadData();
}, [id]);
// ✅ Handler input terkontrol (tidak buat re-render berlebihan)
const handleChange = useCallback(
(field: keyof FormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
const value =
field === 'lakiLaki' || field === 'perempuan'
? Number(e.currentTarget.value)
: e.currentTarget.value;
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// ✅ Submit hanya update global state sekali
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
stateDemografi.update.form = { ...formData };
await stateDemografi.update.submit();
toast.success('Data berhasil diperbarui');
router.push('/admin/ekonomi/demografi-pekerjaan');
} catch (error) {
@@ -160,5 +166,3 @@ function EditDemografiPekerjaan() {
</Box>
);
}
export default EditDemografiPekerjaan;

View File

@@ -126,9 +126,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Laki - Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh style={{ minWidth: 200 }}>Pekerjaan</TableTh>
<TableTh style={{ minWidth: 200 }}>Laki - Laki</TableTh>
<TableTh style={{ minWidth: 200 }}>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
@@ -137,9 +137,9 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.pekerjaan}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.lakiLaki}</TableTd>
<TableTd style={{ minWidth: 200 }}>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"

View File

@@ -1,23 +1,43 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash, IconPlus } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
import HeaderSearch from '../../_com/header';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
// ✅ BarChart Mantine
import { BarChart } from '@mantine/charts';
function JumlahPendudukMiskin() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Jumlah Penduduk Miskin'
placeholder='Cari tahun atau jumlah penduduk miskin...'
title="Jumlah Penduduk Miskin"
placeholder="Cari tahun atau jumlah penduduk miskin..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -28,7 +48,7 @@ function JumlahPendudukMiskin() {
}
function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
type JPMGrafik = { year: number; totalPoorPopulation: number };
const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]);
const [mounted, setMounted] = useState(false);
@@ -36,33 +56,27 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const isTablet = useMediaQuery('(max-width:1024px)');
const isMobile = useMediaQuery('(max-width:768px)');
const { data, page, loading, load, totalPages } = stateJPM.findMany;
const {
data,
page,
loading,
load,
totalPages,
} = stateJPM.findMany;
// Load data
// Load data awal
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
// Update chart data
useEffect(() => {
if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map(item => ({
id: item.id,
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation)
})));
setChartData(
stateJPM.findMany.data.map((item) => ({
year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation),
}))
);
}
}, [stateJPM.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
const handleDelete = () => {
if (selectedId) {
@@ -71,7 +85,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
setSelectedId(null);
stateJPM.findMany.load();
}
}
};
if (loading || !data) {
return (
@@ -83,15 +97,18 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
return (
<Box py={10}>
{/* Tabel */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Jumlah Penduduk Miskin</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconEdit size={18} />}
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')
}
>
Tambah Baru
</Button>
@@ -109,22 +126,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? filteredData.map(item => (
<TableTr key={item.id}>
<TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd>
<Button variant='light' color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button variant='light' color="red" disabled={stateJPM.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true) }}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
)) : (
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)
}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
@@ -138,6 +171,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -153,33 +187,38 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
/>
</Center>
{/* Chart */}
{/* Bar Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Box mt="lg" style={{ width: '100%', minHeight: 350 }}>
<Title order={4} mb="sm">Grafik Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData}>
<XAxis dataKey="year" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
<Title order={4} mb="sm">
Grafik Jumlah Penduduk Miskin
</Title>
{mounted && chartData.length > 0 ? (
<BarChart
h={300}
data={chartData.map((item) => ({
name: item.year.toString(),
value: item.totalPoorPopulation,
}))}
dataKey="name"
series={[
{ name: 'value', color: colors['blue-button'] },
]}
withTooltip
valueFormatter={(v) => `${v.toLocaleString()} jiwa`}
/>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus data ini?'
text="Apakah anda yakin ingin menghapus data ini?"
/>
</Box>
);

View File

@@ -1,24 +1,42 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Center,
Flex,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("");
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='Cari data pendidikan...'
title="Detail Data Pengangguran Berdasarkan Pendidikan"
placeholder="Cari data pendidikan..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +49,6 @@ function GrafikBerdasarkanPendidikan() {
function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
@@ -45,37 +62,45 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
if (stategrafik.findMany.data) {
const SD = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SD || 0), 0);
const SMP = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMP || 0), 0);
const SMA = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.SMA || 0), 0);
const D3 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.D3 || 0), 0);
const S1 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.S1 || 0), 0);
const SD = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SD || 0),
0,
);
const SMP = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SMP || 0),
0,
);
const SMA = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.SMA || 0),
0,
);
const D3 = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.D3 || 0),
0,
);
const S1 = stategrafik.findMany.data.reduce(
(acc: number, cur: any) => acc + Number(cur.S1 || 0),
0,
);
setDonutData([
{ name: 'SD', value: SD, color: '#4b6Ef5', key: 'SD' },
{ name: 'SMP', value: SMP, color: '#14b885', key: 'SMP' },
{ name: 'SMA', value: SMA, color: '#E6A03B', key: 'SMA' },
{ name: 'D3', value: D3, color: '#DB524D', key: 'D3' },
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
{ name: 'SD', value: SD, color: '#4b6Ef5' },
{ name: 'SMP', value: SMP, color: '#14b885' },
{ name: 'SMA', value: SMA, color: '#E6A03B' },
{ name: 'D3', value: D3, color: '#DB524D' },
{ name: 'S1', value: S1, color: '#1018A8FF' },
]);
}
}, [stategrafik.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
@@ -87,21 +112,26 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
return (
<Box py={10}>
{/* Table Data */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Title order={4}>List Pengangguran Berdasarkan Pendidikan</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create')}
onClick={() =>
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create',
)
}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
@@ -120,7 +150,9 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTr>
<TableTd colSpan={7}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
<Text color="dimmed">
Belum ada data grafik responden
</Text>
</Center>
</TableTd>
</TableTr>
@@ -134,7 +166,15 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
<TableTd>{item.S1}</TableTd>
<TableTd>
<Tooltip label="Edit Data" withArrow>
<Button color="green" variant="light" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
<Button
color="green"
variant="light"
onClick={() =>
router.push(
`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`,
)
}
>
<IconEdit size={18} />
</Button>
</Tooltip>
@@ -148,7 +188,8 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}>
}}
>
<IconTrash size={18} />
</Button>
</Tooltip>
@@ -161,6 +202,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -176,51 +218,35 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
/>
</Center>
{/* Chart */}
<Box mt="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', minHeight: 250 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie
dataKey="value"
nameKey="name"
data={donutData}
cx={400}
cy={150}
innerRadius={60}
outerRadius={115}
label
>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack gap="xs" mt="sm">
{donutData.map((entry) => (
<Flex key={entry.key} gap="sm" align="center">
<Box w={20} h={20} bg={entry.color} />
<Text>{entry.name} : {entry.value}</Text>
</Flex>
))}
</Stack>
</Box>
) : (
<Text color="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
</Box>
{/* Donut Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md" mt="md">
<Stack>
<Title order={3} pb={10}>
Grafik Pengangguran Berdasarkan Pendidikan
</Title>
{donutData.length > 0 ? (
<DonutChart
data={donutData}
withLabels
withTooltip
tooltipDataSource="segment"
size={260}
thickness={40}
/>
) : (
<Text color="dimmed">
Belum ada data untuk ditampilkan dalam grafik
</Text>
)}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
{/* Modal Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?'
text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan pendidikan ini?"
/>
</Box>
);

View File

@@ -1,13 +1,30 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import {
Box,
Button,
Center,
Flex,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Cell, Pie, PieChart } from 'recharts';
import { useProxy } from 'valtio/utils';
import { DonutChart } from '@mantine/charts';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
@@ -17,8 +34,8 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
return (
<Box>
<HeaderSearch
title='Detail Data Pengangguran'
placeholder='Cari usia...'
title="Detail Data Pengangguran"
placeholder="Cari usia..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -31,7 +48,6 @@ function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
@@ -45,17 +61,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
}
};
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
const { data, page, totalPages, loading, load } = stategrafik.findMany;
useShallowEffect(() => {
setMounted(true);
load(page, 10, search)
load(page, 10, search);
}, [page, search]);
useEffect(() => {
@@ -64,16 +73,17 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
const totalUsia26_35 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia26_35 || 0), 0);
const totalUsia36_45 = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia36_45 || 0), 0);
const totalUsia46_keatas = stategrafik.findMany.data.reduce((acc: number, cur: any) => acc + Number(cur.usia46_keatas || 0), 0);
setDonutData([
{ name: 'usia18_25', value: totalUsia18_25, color: colors['blue-button'], key: 'usia18_25' },
{ name: 'usia26_35', value: totalUsia26_35, color: '#10A85AFF', key: 'usia26_35' },
{ name: 'usia36_45', value: totalUsia36_45, color: '#C07B13FF', key: 'usia36_45' },
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
{ name: 'Usia 18-25', value: totalUsia18_25, color: colors['blue-button'] },
{ name: 'Usia 26-35', value: totalUsia26_35, color: '#10A85AFF' },
{ name: 'Usia 36-45', value: totalUsia36_45, color: '#C07B13FF' },
{ name: 'Usia 46+', value: totalUsia46_keatas, color: '#1094A8FF' },
]);
}
}, [stategrafik.findMany.data]);
const filteredData = data || []
const filteredData = data || [];
if (loading || !data) {
return (
@@ -85,24 +95,23 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
return (
<Box py={10}>
{/* Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
{/* Header */}
<Flex justify="space-between" align="center" mb="md">
<Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
<Tooltip label="Tambah Data" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
>
Tambah Baru
</Button>
</Tooltip>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')
}
>
Tambah Baru
</Button>
</Flex>
{/* Table */}
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
@@ -110,26 +119,38 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
<TableTh>Usia 18-25</TableTh>
<TableTh>Usia 26-35</TableTh>
<TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Usia 46+</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map(item => (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia26_35}</TableTd>
<TableTd>{item.usia36_45}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd>
<TableTd>
<Button color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
<Button
color="green"
onClick={() =>
router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)
}
>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button color="red" disabled={stategrafik.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true); }}>
<Button
color="red"
disabled={stategrafik.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={20} />
</Button>
</TableTd>
@@ -147,10 +168,10 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
@@ -166,48 +187,29 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
/>
</Center>
{/* Chart */}
{/* Donut Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie dataKey="value" nameKey="name" data={donutData} cx={400} cy={150} innerRadius={60} outerRadius={115} label>
{donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack mt="sm" gap="xs">
<Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
</Text>
</Flex>
</Stack>
</Box>
<Title order={3} pb={10}>
Grafik Pengangguran Berdasarkan Usia Kerja
</Title>
{donutData.length > 0 ? (
<Center>
<DonutChart
data={donutData}
withLabels
withTooltip
size={200}
thickness={40}
tooltipDataSource="segment"
/>
</Center>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors';
import {
@@ -20,11 +21,18 @@ import { useEffect, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
// --- Helper konstanta
const MONTHS = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des',
];
function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter();
const params = useParams();
// --- state lokal form
const [formData, setFormData] = useState({
month: '',
year: new Date().getFullYear(),
@@ -34,18 +42,13 @@ function EditDetailDataPengangguran() {
percentageChange: 0,
});
// Hitung total & perubahan otomatis
// --- hitung total + persentase perubahan
const calculateTotalAndChange = useCallback(
async (data: typeof formData) => {
const total = data.educatedUnemployment + data.uneducatedUnemployment;
let percentageChange = 0;
const monthOrder = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des',
];
const currentMonthIndex = monthOrder.indexOf(data.month);
const currentMonthIndex = MONTHS.indexOf(data.month);
if (currentMonthIndex !== -1) {
let prevMonthIndex = currentMonthIndex - 1;
let prevYear = data.year;
@@ -55,17 +58,15 @@ function EditDetailDataPengangguran() {
prevYear--;
}
const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({
month: prevMonth,
month: MONTHS[prevMonthIndex],
year: prevYear,
});
if (prevData && prevData.totalUnemployment > 0) {
const change =
((total - prevData.totalUnemployment) /
prevData.totalUnemployment) *
100;
prevData.totalUnemployment) * 100;
percentageChange = parseFloat(change.toFixed(1));
}
}
@@ -75,67 +76,66 @@ function EditDetailDataPengangguran() {
[stateDetail.findByMonthYear]
);
// --- update state lokal
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange(newData);
setFormData({
...newData,
totalUnemployment: total,
percentageChange,
});
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
// Load detail hanya sekali
// --- load detail by ID (sekali)
useEffect(() => {
const loadDetail = async () => {
const id = params?.id as string;
if (!id) return;
try {
await stateDetail.findUnique.load(id); // ambil by ID
await stateDetail.findUnique.load(id);
const data = stateDetail.findUnique.data;
if (!data) return;
if (data) {
const yearValue =
data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
const yearValue =
data.year && typeof data.year === 'object' && 'getFullYear' in data.year
? (data.year as Date).getFullYear()
: Number(data.year);
stateDetail.update.id = id; // set ID untuk update
stateDetail.update.id = id; // simpan id untuk update
setFormData({
month: data.month,
year: yearValue,
totalUnemployment: data.totalUnemployment,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
percentageChange: data.percentageChange || 0,
});
}
} catch (error) {
console.error('Error loading detail:', error);
setFormData({
month: data.month,
year: yearValue,
educatedUnemployment: data.educatedUnemployment,
uneducatedUnemployment: data.uneducatedUnemployment,
totalUnemployment: data.totalUnemployment,
percentageChange: data.percentageChange || 0,
});
} catch (err) {
console.error('Error loading detail:', err);
toast.error('Gagal memuat data detail');
}
};
loadDetail();
}, [params?.id, stateDetail.findUnique]);
}, [params?.id]);
// --- submit form
const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange(formData);
try {
const { total, percentageChange } = await calculateTotalAndChange(formData);
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');
}
} catch (error) {
console.error('Error updating:', error);
} catch (err) {
console.error('Error updating:', err);
toast.error('Terjadi kesalahan saat memperbarui data');
}
};
@@ -143,12 +143,7 @@ function EditDetailDataPengangguran() {
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm">
@@ -167,10 +162,7 @@ function EditDetailDataPengangguran() {
<Stack gap="md">
<Select
label="Bulan"
data={[
'Jan','Feb','Mar','Apr','Mei','Jun',
'Jul','Agu','Sep','Okt','Nov','Des',
]}
data={MONTHS}
value={formData.month}
onChange={(val) => updateFormData({ month: val || '' })}
/>
@@ -184,8 +176,10 @@ function EditDetailDataPengangguran() {
label="Pengangguran Terdidik"
type="number"
value={formData.educatedUnemployment}
onChange={(val) =>
updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })
onChange={(e) =>
updateFormData({
educatedUnemployment: Number(e.currentTarget.value) || 0,
})
}
required
/>
@@ -193,8 +187,10 @@ function EditDetailDataPengangguran() {
label="Pengangguran Tidak Terdidik"
type="number"
value={formData.uneducatedUnemployment}
onChange={(val) =>
updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })
onChange={(e) =>
updateFormData({
uneducatedUnemployment: Number(e.currentTarget.value) || 0,
})
}
required
/>

View File

@@ -33,6 +33,7 @@ function EditLowonganKerja() {
gaji: '',
deskripsi: '',
kualifikasi: '',
notelp: '',
});
// load data sekali aja ketika mount / id berubah
@@ -52,6 +53,7 @@ function EditLowonganKerja() {
gaji: data.gaji || '',
deskripsi: data.deskripsi || '',
kualifikasi: data.kualifikasi || '',
notelp: data.notelp || '',
});
}
} catch (error) {
@@ -132,6 +134,14 @@ function EditLowonganKerja() {
required
/>
<TextInput
label="Nomor Yang Dapat Dihubungi"
placeholder="Masukkan nomor yang dapat dihubungi"
value={formData.notelp}
onChange={(e) => handleChange("notelp", e.target.value)}
required
/>
<TextInput
label="Tipe Pekerjaan"
placeholder="Masukkan tipe pekerjaan"

View File

@@ -82,6 +82,11 @@ function DetailLowonganKerjaLokal() {
<Text fz="md" c="dimmed">{data.lokasi || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Nomor Yang Dapat Dihubungi</Text>
<Text fz="md" c="dimmed">{data.notelp || '-'}</Text>
</Box>
<Box>
<Text fz="lg" fw="bold">Tipe Pekerjaan</Text>
<Text fz="md" c="dimmed">{data.tipePekerjaan || '-'}</Text>

View File

@@ -30,6 +30,7 @@ function CreateLowonganKerja() {
gaji: '',
deskripsi: '',
kualifikasi: '',
notelp: '',
};
};
@@ -86,6 +87,15 @@ function CreateLowonganKerja() {
placeholder="Masukkan nama perusahaan"
required
/>
<TextInput
defaultValue={lowonganState.create.form.notelp}
onChange={(val) =>
(lowonganState.create.form.notelp = val.target.value)
}
label="Nomor Yang Dapat Dihubungi"
placeholder="Masukkan nomor yang dapat dihubungi"
required
/>
<TextInput
defaultValue={lowonganState.create.form.lokasi}
onChange={(val) =>

View File

@@ -18,84 +18,90 @@ import {
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
type Statistik = {
tahun: string;
jumlah: string;
};
type FormData = {
nama: string;
deskripsi: string;
icon: string;
statistik: Statistik;
};
const initialForm: FormData = {
nama: '',
deskripsi: '',
icon: '',
statistik: {
tahun: string;
jumlah: string;
};
tahun: '',
jumlah: '',
},
};
function EditProgramKemiskinan() {
const router = useRouter();
const params = useParams() as { id: string };
const { id } = useParams() as { id: string };
const stateProgram = useProxy(programKemiskinanState);
const id = params.id;
const [formData, setFormData] = useState<FormData>({
nama: '',
deskripsi: '',
icon: '',
statistik: {
tahun: '',
jumlah: '',
},
});
const [formData, setFormData] = useState<FormData>(initialForm);
// load data ke local state sekali aja
// Load data 1x dari global state → isi local state
useEffect(() => {
if (id) {
stateProgram.findUnique
.load(id)
.then(() => {
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
nama: data.nama || '',
deskripsi: data.deskripsi || '',
icon: data.icon || '',
statistik: {
tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '',
},
});
}
})
.catch((err) => {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}
if (!id) return;
stateProgram.findUnique
.load(id)
.then(() => {
const data = stateProgram.findUnique.data;
if (data) {
setFormData({
nama: data.nama ?? '',
deskripsi: data.deskripsi ?? '',
icon: data.icon ?? '',
statistik: {
tahun: data.statistik?.tahun?.toString() ?? '',
jumlah: data.statistik?.jumlah?.toString() ?? '',
},
});
}
})
.catch((err) => {
console.error('Error load data:', err);
toast.error('Gagal mengambil data program');
});
}, [id, stateProgram.findUnique]);
const handleChange = (field: keyof FormData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
// generic handler untuk field top-level
const handleChange = useCallback(
(field: keyof FormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
const handleStatistikChange = (field: keyof FormData['statistik'], value: string) => {
setFormData((prev) => ({
...prev,
statistik: {
...prev.statistik,
[field]: value,
},
}));
};
// khusus nested statistik
const handleStatistikChange = useCallback(
(field: keyof Statistik, value: string) => {
setFormData((prev) => ({
...prev,
statistik: { ...prev.statistik, [field]: value },
}));
},
[]
);
const handleSubmit = async () => {
try {
stateProgram.update.id = id;
stateProgram.update.form = formData;
await stateProgram.update.update();
toast.success('Program berhasil diperbarui!');
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {

View File

@@ -40,7 +40,7 @@ const DetailDataPengangguran = new Elysia({
percentageChange: t.Optional(t.Number()),
}),
})
.delete("/:id", detailDataPengangguranDelete, {
.delete("/del/:id", detailDataPengangguranDelete, {
params: t.Object({
id: t.String(),
}),

View File

@@ -9,6 +9,7 @@ type FormCreate = {
gaji: string;
deskripsi: string;
kualifikasi: string;
notelp: string;
}
export default async function lowonganKerjaCreate(context: Context) {
@@ -23,6 +24,7 @@ export default async function lowonganKerjaCreate(context: Context) {
gaji: body.gaji,
deskripsi: body.deskripsi,
kualifikasi: body.kualifikasi,
notelp: body.notelp,
},
});

View File

@@ -15,6 +15,7 @@ const LowonganKerja = new Elysia({prefix: "/lowongankerja", tags: ["Ekonomi/Lowo
gaji: t.String(),
deskripsi: t.String(),
kualifikasi: t.String(),
notelp: t.String(),
})
})
.get("/find-many", lowonganKerjaFindMany)
@@ -35,6 +36,7 @@ const LowonganKerja = new Elysia({prefix: "/lowongankerja", tags: ["Ekonomi/Lowo
gaji: t.String(),
deskripsi: t.String(),
kualifikasi: t.String(),
notelp: t.String(),
})
})

View File

@@ -9,6 +9,7 @@ type FormUpdate = {
gaji: string;
deskripsi: string;
kualifikasi: string;
notelp: string;
}
export default async function lowonganKerjaUpdate(context: Context){
@@ -16,7 +17,7 @@ export default async function lowonganKerjaUpdate(context: Context){
const id = context.params?.id;
const body = context.body as FormUpdate;
const { posisi, namaPerusahaan, lokasi, tipePekerjaan, gaji, deskripsi, kualifikasi } = body;
const { posisi, namaPerusahaan, lokasi, tipePekerjaan, gaji, deskripsi, kualifikasi, notelp } = body;
if (!id) {
return Response.json({
@@ -46,6 +47,7 @@ export default async function lowonganKerjaUpdate(context: Context){
gaji,
deskripsi,
kualifikasi,
notelp,
},
});

View File

@@ -1,83 +1,52 @@
import colors from '@/con/colors';
import { Stack, Box, Container, Grid, GridCol, Group, Paper, TextInput, Text, Image, Flex, Button } from '@mantine/core';
import { IconCalendar, IconMapPin, IconSearch, IconUsersGroup } from '@tabler/icons-react';
import { Stack, Container, Box, List, ListItem, Text, Image } from '@mantine/core';
import React from 'react';
import BackButton from '../darmasaba/(pages)/desa/layanan/_com/BackButto';
import Link from 'next/link';
function Page() {
return (
<Stack pos="relative" bg={colors.Bg} py="xl" gap="md">
{/* Header */}
<Container size="lg" px="md">
<Stack align="center" gap={0} mb="xl">
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Program Gotong Royong
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
<Box px={{ base: "md", md: 100 }}><BackButton /></Box>
<Container w={{ base: "100%", md: "50%" }} >
<Box pb={20}>
<Text ta={"center"} fz={"3.4rem"} c={colors["blue-button"]} fw={"bold"}>
Bumdes Pudak Mesari
</Text>
<Text fz={{ base: "2rem", md: "3.4rem" }} c={colors["blue-button"]} fw="bold" ta="center">
Desa Darmasaba
<Text
ta={"center"}
fw={"bold"}
fz={"1.5rem"}
>
Informasi dan Pelayanan Administrasi Digital
</Text>
</Stack>
</Box>
<Image src="/api/img/ack.png" alt='' w={"100%"} />
</Container>
{/* Tabs Menu */}
<Box px={{ base: "md", md: "xl" }} py="md" bg={colors['BG-trans']} mb="md">
<Grid align="center" justify="space-between" mb={20}>
<GridCol span={{ base: 12, md: 8 }}>
<Group gap="md" wrap="wrap">
<Paper bg={colors['blue-button']} radius="xl" py={5} px={20}>
<Text c={colors['white-1']} size="sm">
Semua
</Text>
</Paper>
{['Kebersihan', 'Infrastruktur', 'Sosial', 'Lingkungan'].map((kategori) => (
<Paper key={kategori} bg={colors['blue-button-trans']} radius="xl" py={5} px={20}>
<Text size="sm">
{kategori}
</Text>
</Paper>
))}
</Group>
</GridCol>
<GridCol span={{ base: 12, md: 4 }}>
<TextInput
radius="lg"
placeholder="Cari Program Gotong Royong"
leftSection={<IconSearch size={18} />}
w="100%"
/>
</GridCol>
</Grid>
<Paper p={"xl"} bg={colors['white-trans-1']} w={{ base: "100%", md: "100%" }}>
<Stack gap={'xs'}>
<Image radius={20} src={'/api/img/gotong-royong.png'} w={'100%'} alt='' />
<Text fw={"bold"} fz={{ base: "h2", md: "h1" }}>Membangun Fasilitas Desa</Text>
<Group>
<Paper py={5} px={20} bg={colors['blue-button-trans']} radius={20}>
<Text c={colors['white-1']}>Sosial</Text>
</Paper>
</Group>
<Text fz={{ base: "h4", md: "h3" }}>
Program Pembangunan Fasilitas Desa Maju, Masyarakat Sejahtera.
</Text>
<Flex gap={5} align={'center'}>
<IconCalendar color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>1 April 2025</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconMapPin color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>Banjar Desa Darmasaba</Text>
</Flex>
<Flex gap={5} align={'center'}>
<IconUsersGroup color={colors['blue-button-trans']} size={45} />
<Text fz={{ base: "h4", md: "h3" }}>30 Partisipan</Text>
</Flex>
<Text fw={'bold'} fz={'md'}>Deskripsi : Program pembangunan Pura sebagai pusat spiritual dan budaya desa, melibatkan gotong royong masyarakat dalam pembangunan struktur utama serta ornamen tradisional.</Text>
<Group py={20} justify='center'>
<Button component={Link} href={'https://www.whatsapp.com/?lang=id'} bg={colors['blue-button']} >Daftar Sebagai Relawan</Button>
</Group>
</Stack>
</Paper>
<Box px={{ base: "md", md: 100 }}>
<Text py={20} fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Badan Usaha Milik Desa (BUMDes) Pudak Mesari adalah lembaga ekonomi desa yang berperan penting dalam pengembangan potensi dan kesejahteraan masyarakat Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali. BUMDes ini berfungsi sebagai motor penggerak perekonomian desa melalui berbagai unit usaha yang dikelola secara profesional.
</Text>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Potensi dan Peran BUMDes Pudak Mesari:
</Text>
<List py={20} type='ordered'>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengembangan Usaha Mikro dan Kecil:</Text>BUMDes Pudak Mesari menyediakan layanan bagi pelaku usaha mikro dan kecil di desa, seperti penyediaan konsumsi dan snack kotak untuk berbagai acara.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Pengelolaan Sampah Berbasis Masyarakat:</Text>Melalui kolaborasi dengan komunitas pemuda peduli lingkungan, BUMDes Pudak Mesari aktif dalam pengelolaan sampah berbasis masyarakat.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Peningkatan Kapasitas dan Transparansi:</Text>Untuk memastikan pengelolaan yang akuntabel, BUMDes Pudak Mesari rutin mengadakan rapat koordinasi dan pendampingan penyusunan laporan pertanggungjawaban.
</ListItem>
<ListItem fz={{ base: "sm", md: "lg" }} ta={"justify"}>
<Text fz={{ base: "sm", md: "lg" }} fw={"bold"}>Kolaborasi Internasional:</Text>Desa Darmasaba, melalui BUMDes Pudak Mesari, menerima kunjungan dari tim Osaki Jepang untuk memperkuat pengelolaan sampah dan lingkungan.
</ListItem>
</List>
<Text fz={{ base: "sm", md: "lg" }} ta={"justify"}>
Dengan berbagai inisiatif tersebut, BUMDes Pudak Mesari menunjukkan perannya sebagai pilar utama dalam pengembangan ekonomi dan kesejahteraan masyarakat Desa Darmasaba, sekaligus menjaga kelestarian lingkungan melalui program-program inovatif dan kolaboratif.
</Text>
</Box>
</Stack>
);

View File

@@ -165,7 +165,7 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Table, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
@@ -206,8 +206,117 @@ function Page() {
<Stack gap="lg" justify="center">
<Paper bg={colors['white-1']} p="xl">
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
{/* Pendapatan, Belanja, Pembiayaan Card sama seperti sebelumnya */}
{/* ... */}
{/* Pendapatan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pendapatan</Title>
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c={colors['blue-button']}>
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPendapatan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Belanja Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Belanja</Title>
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="orange">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalBelanja)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
{/* Pembiayaan Card */}
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
<Stack gap={"xs"}>
<Title order={3}>Pembiayaan</Title>
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
<Box key={item.id}>
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{item.name}</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(item.value)}</Text>
</GridCol>
</Grid>
</Box>
))}
<Grid>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
</GridCol>
<GridCol span={{ base: 12, md: 6 }}>
<Text fz="xl" fw={700} c="green">
{new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(totalPembiayaan)}
</Text>
</GridCol>
</Grid>
</Stack>
</Box>
</SimpleGrid>
</Paper>
@@ -218,7 +327,7 @@ function Page() {
<Table.Thead>
<Table.Tr>
<Table.Th>Keterangan</Table.Th>
<Table.Th align="right">Jumlah</Table.Th>
<Table.Th ta={"right"}>Jumlah</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>

View File

@@ -86,38 +86,41 @@ function Page() {
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Usia</Text>
{mounted && donutGrafikNganggurData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<PieChart
size={300}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurData}
withTooltip
tooltipDataSource="segment"
mx="auto" />
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart
w="100%"
h={250} // lebih kecil biar aman di mobile
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurData}
withTooltip
tooltipDataSource="segment"
/>
</Box>
</Box>) : <Skeleton h={500} />}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>18-25</Text>
<ColorSwatch color="#4b6Ef5" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>26-35</Text>
<ColorSwatch color="#14b885" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>36-45</Text>
<ColorSwatch color="#E6A03B" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>46+</Text>
<ColorSwatch color="#DB524D" size={30} />
</Flex>
@@ -127,44 +130,47 @@ function Page() {
<Paper p={'lg'}>
<Text fw={'bold'} fz={'h3'}>Pengangguran Berdasarkan Pendidikan</Text>
{mounted2 && donutGrafikNganggurDataPendidikan.length > 0 ? (<Center>
<PieChart
size={300}
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurDataPendidikan}
withTooltip
tooltipDataSource="segment"
mx="auto" />
<Box w="100%" style={{ maxWidth: 400, margin: "0 auto" }}>
<PieChart
w="100%"
h={250} // lebih kecil biar aman di mobile
withLabelsLine
labelsPosition="outside"
labelsType="percent"
withLabels
data={donutGrafikNganggurDataPendidikan}
withTooltip
tooltipDataSource="segment"
/>
</Box>
</Center>) : <Skeleton h={500} />}
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'} wrap="wrap">
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SD</Text>
<ColorSwatch color="#4b6Ef5" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMP</Text>
<ColorSwatch color="#14b885" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>SMA/SMK</Text>
<ColorSwatch color="#E6A03B" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>D3</Text>
<ColorSwatch color="#DB524D" size={30} />
</Flex>
</Box>
<Box>
<Flex gap={{ base: 5, md: 8 }} align={'center'}>
<Flex gap={{ base: 5, md: 8 }} align={'center'} wrap="wrap">
<Text fw={'bold'} fz={{ base: 'md', md: 'h4' }}>S1</Text>
<ColorSwatch color="#1018A8FF" size={30} />
</Flex>

View File

@@ -103,7 +103,7 @@ function Page() {
</Box>
</Flex>
</Box>
<Button onClick={() => router.push('https://www.whatsapp.com/?lang=id')} bg={colors['blue-button']}>Lamar Sekarang</Button>
<Button onClick={() => router.push(`https://wa.me/${v.notelp?.replace(/\D/g, '')}`)}>Lamar Sekarang</Button>
</Stack>
</Paper>
)

View File

@@ -87,7 +87,7 @@ function Page() {
<Box>
<Select
placeholder="Pilih Kategori"
data={pasarDesaState.kategoriProduk.findMany.data?.map((v) => ({
data={pasarDesaState.kategoriProduk.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama
})) || []}

View File

@@ -104,9 +104,7 @@ function Potensi() {
{v.name}
</Text>
</Tooltip>
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }}>
{v.deskripsi}
</Text>
<Text lineClamp={2} c="gray.2" fz={{ base: "0.8rem", md: "1rem" }} dangerouslySetInnerHTML={{ __html: v.deskripsi }} />
</Stack>
</BackgroundImage>
</motion.div>