QC Admin - User Menu Ekonomi : Jumlah Pengangguran

This commit is contained in:
2025-09-16 10:11:54 +08:00
parent a5d841bb6b
commit 4ceea5203f
97 changed files with 6023 additions and 3481 deletions

View File

@@ -2,7 +2,16 @@
'use client'
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -72,41 +81,72 @@ function EditBelanja() {
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jenis Belanja
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Edit Jenis Pendapatan</Title>
<TextInput
value={formData.name}
onChange={(val) => {
setFormData({ ...formData, name: val.target.value });
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Pendapatan</Text>}
placeholder='Masukkan nama Jenis Pendapatan'
/>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
value={formatRupiah(formData.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
</Group>
</Stack>
</Paper>
</Box>
{/* Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Jenis Belanja"
placeholder="Masukkan nama jenis belanja"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
/>
<TextInput
label="Nilai"
placeholder="Masukkan nilai"
value={formatRupiah(formData.value)}
onChange={(e) => {
const raw = e.currentTarget.value;
const cleanValue = unformatRupiah(raw);
setFormData({ ...formData, value: cleanValue });
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditBelanja;
export default EditBelanja;

View File

@@ -1,17 +1,30 @@
'use client'
'use client';
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function CreateBelanja() {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const router = useRouter()
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const formatRupiah = (value: number | string) => {
const number = typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
const number =
typeof value === 'number' ? value : Number(value.replace(/\D/g, ''));
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
@@ -25,48 +38,83 @@ function CreateBelanja() {
const resetForm = () => {
belanjaState.create.form = {
name: "",
name: '',
value: 0,
}
}
};
};
const handleSubmit = async () => {
await belanjaState.create.submit();
resetForm()
router.push("/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja")
}
return (
<Box>
<Box mb={10}>
<Button onClick={() => router.back()} variant='subtle' color={'blue'}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
if (!belanjaState.create.form.name || !belanjaState.create.form.value) {
return toast.warn('Lengkapi semua field terlebih dahulu');
}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Stack gap={"xs"}>
<Title order={4}>Create Jenis Belanja</Title>
await belanjaState.create.submit();
resetForm();
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
{/* Header dengan back button */}
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Jenis Belanja
</Title>
</Group>
{/* Card Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label={<Text fw="bold" fz="sm">Nama Jenis Belanja</Text>}
placeholder="Masukkan nama jenis belanja"
value={belanjaState.create.form.name}
onChange={(val) => {
belanjaState.create.form.name = val.target.value;
}}
label={<Text fw={"bold"} fz={"sm"}>Nama Jenis Belanja</Text>}
placeholder='Masukkan nama jenis belanja'
onChange={(e) => (belanjaState.create.form.name = e.target.value)}
required
/>
<TextInput
type='text'
type="text"
label={<Text fw="bold" fz="sm">Nilai</Text>}
placeholder="Masukkan nilai belanja"
value={formatRupiah(belanjaState.create.form.value)}
onChange={(val) => {
const raw = val.currentTarget.value;
const cleanValue = unformatRupiah(raw);
belanjaState.create.form.value = cleanValue;
onChange={(e) => {
const raw = e.currentTarget.value;
belanjaState.create.form.value = unformatRupiah(raw);
}}
label={<Text fw={"bold"} fz={"sm"}>Nilai</Text>}
placeholder='Masukkan nilai'
required
/>
<Group>
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>

View File

@@ -1,24 +1,41 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
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 } from '@tabler/icons-react';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import PendapatanAsliDesa from '../../../_state/ekonomi/PADesa';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
function Belanja() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Belanja'
placeholder='pencarian'
title="Belanja"
placeholder="Cari belanja berdasarkan nama atau nilai..."
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,108 +46,175 @@ function Belanja() {
}
function ListBelanja({ search }: { search: string }) {
const belanjaState = useProxy(PendapatanAsliDesa.belanja)
const belanjaState = useProxy(PendapatanAsliDesa.belanja);
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const formatRupiah = (value: number) => {
return new Intl.NumberFormat('id-ID', {
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
loading,
load,
page,
totalPages,
} = belanjaState.findMany;
const formatRupiah = (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
const totalBelanja = belanjaState.findMany.data.reduce((sum, item) => sum + item.value, 0);
const totalBelanja = data?.reduce((sum, item) => sum + item.value, 0) || 0;
const handleDelete = () => {
if (selectedId) {
belanjaState.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
belanjaState.findMany.load()
belanjaState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
}
};
useShallowEffect(() => {
belanjaState.findMany.load();
}, [])
load(page, 10, search);
}, [page, search]);
const filteredData = (belanjaState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.value.toString().toLowerCase().includes(keyword)
);
});
const filteredData = data || [];
if (!belanjaState.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
<Skeleton height={600} radius="md" />
</Stack>
)
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Belanja'
href='/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>{((item.value / totalBelanja) * 100).toFixed(0)}%</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>Daftar Belanja</Title>
<Tooltip label="Tambah Belanja" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() =>
router.push('/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/create')
}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Nilai</TableTh>
<TableTh>Persentase</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
))}
<TableTr>
<TableTd colSpan={4}>
<Text fw={'bold'}>Total</Text>
</TableTd>
<TableTd>
{formatRupiah(belanjaState.findMany.data.reduce((total, item) => total + item.value, 0))}
</TableTd>
</TableTr>
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
<>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd>{formatRupiah(item.value)}</TableTd>
<TableTd>
{totalBelanja > 0
? ((item.value / totalBelanja) * 100).toFixed(0) + '%'
: '0%'}
</TableTd>
<TableTd>
<Group gap="xs">
<Tooltip label="Edit" withArrow>
<Button
size="xs"
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/ekonomi/PADesa-pendapatan-asli-desa/belanja/${item.id}`
)
}
>
<IconEdit size={16} />
</Button>
</Tooltip>
<Tooltip label="Hapus" withArrow>
<Button
size="xs"
variant="light"
color="red"
disabled={belanjaState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={16} />
</Button>
</Tooltip>
</Group>
</TableTd>
</TableTr>
))}
<TableTr>
<TableTd colSpan={2}>
<Text fw="bold">Total</Text>
</TableTd>
<TableTd colSpan={2}>
<Text fw="bold">{formatRupiah(totalBelanja)}</Text>
</TableTd>
</TableTr>
</>
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data belanja yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus belanja ini?'
text="Apakah anda yakin ingin menghapus belanja ini?"
/>
</Box>
);