QC Tampilan Admin & User, Api berfungsi

This commit is contained in:
2025-09-16 16:47:12 +08:00
parent 4ceea5203f
commit 39e1e7b575
48 changed files with 3250 additions and 1916 deletions

View File

@@ -1,8 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, 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 } from 'react';
@@ -11,33 +20,33 @@ import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() {
const router = useRouter()
const params = useParams() as { id: string }
const stateDemografi = useProxy(demografiPekerjaan)
const router = useRouter();
const params = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id
const id = params.id;
useEffect(() => {
if (!id) return;
stateDemografi.update.id = id;
stateDemografi.findUnique.load(id)
stateDemografi.findUnique
.load(id)
.then(() => {
const data = stateDemografi.findUnique.data;
if (data) {
stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0)
perempuan: Number(data.perempuan || 0),
};
}
})
.catch(error => {
.catch((error) => {
console.error('Error loading data:', error);
toast.error('Gagal memuat data');
});
}, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => {
try {
stateDemografi.update.id = id;
@@ -51,52 +60,88 @@ function EditDemografiPekerjaan() {
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Demografi Pekerjaan</Title>
<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 Demografi Pekerjaan
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Pekerjaan"
placeholder="masukkan pekerjaan"
placeholder="Masukkan jenis pekerjaan"
value={stateDemografi.update.form.pekerjaan}
onChange={(val) => {
stateDemografi.update.form.pekerjaan = val.currentTarget.value;
}}
onChange={(e) =>
(stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}
required
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
label="Jumlah Pekerja Laki-laki"
type="number"
placeholder="masukkan jumlah pekerja laki - laki"
placeholder="Masukkan jumlah pekerja laki-laki"
value={stateDemografi.update.form.lakiLaki}
onChange={(val) => {
stateDemografi.update.form.lakiLaki = Number(val.currentTarget.value);
}}
onChange={(e) =>
(stateDemografi.update.form.lakiLaki = Number(
e.currentTarget.value
))
}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
placeholder="masukkan jumlah pekerja perempuan"
placeholder="Masukkan jumlah pekerja perempuan"
value={stateDemografi.update.form.perempuan}
onChange={(val) => {
stateDemografi.update.form.perempuan = Number(val.currentTarget.value);
}}
onChange={(e) =>
(stateDemografi.update.form.perempuan = Number(
e.currentTarget.value
))
}
required
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan Perubahan
</Button>
<Group justify="flex-end">
<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 EditDemografiPekerjaan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
@@ -12,15 +22,15 @@ import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const router = useRouter();
const resetForm = () => {
stateDemografi.create.form = {
pekerjaan: "",
pekerjaan: '',
lakiLaki: 0,
perempuan: 0,
}
}
};
};
const handleSubmit = async () => {
const id = await stateDemografi.create.create();
@@ -32,58 +42,85 @@ function CreateDemografiPekerjaan() {
}
}
resetForm();
router.push("/admin/ekonomi/demografi-pekerjaan");
}
router.push('/admin/ekonomi/demografi-pekerjaan');
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Demografi Pekerjaan</Title>
<Stack gap={"xs"}>
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
<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">
Tambah Demografi Pekerjaan
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
}}
required
/>
<TextInput
label="Jumlah Pekerja Laki-Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki-laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
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)',
}}
/>
<TextInput
label="Jumlah Pekerja Laki - Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki - laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}

View File

@@ -1,14 +1,32 @@
'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 {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
Flex,
} 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 { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
@@ -18,7 +36,7 @@ function DemografiPekerjaan() {
<Box>
<HeaderSearch
title='Demografi Pekerjaan'
placeholder='pencarian'
placeholder='Cari pekerjaan atau jumlah pekerja...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,131 +52,193 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
pekerjaan: string;
lakiLaki: number;
perempuan: number;
}
};
const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan)
const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [mounted, setMounted] = useState(false);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const {
data,
page,
totalPages,
loading,
load,
} = stateDemografi.findMany;
const handleDelete = () => {
if (selectedId) {
stateDemografi.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateDemografi.findMany.load()
stateDemografi.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
}
}
};
useShallowEffect(() => {
setMounted(true)
stateDemografi.findMany.load()
}, [])
setMounted(true);
load(page, 10, search);
}, [page, search]);
useEffect(() => {
setMounted(true);
if (stateDemografi.findMany.data) {
setChartData(stateDemografi.findMany.data.map((item) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan),
})));
if (data) {
setChartData(
data.map((item) => ({
id: item.id,
pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan),
}))
);
}
}, [stateDemografi.findMany.data]);
}, [data]);
const filteredData = (stateDemografi.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
const filteredData = data || [];
if (loading || !data) {
return (
item.pekerjaan.toLowerCase().includes(keyword) ||
item.lakiLaki.toString().toLowerCase().includes(keyword) ||
item.perempuan.toString().toLowerCase().includes(keyword)
<Stack py={10}>
<Skeleton height={600} radius="md" />
</Stack>
);
});
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Demografi Pekerjaan'
href='/admin/ekonomi/demografi-pekerjaan/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Jumlah Pekerja Laki - Laki</TableTh>
<TableTh>Jumlah Pekerja Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}>
<IconEdit size={20} />
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateDemografi.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}>List Demografi Pekerjaan</Title>
<Tooltip label="Tambah Data Pekerjaan" withArrow>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr>
<TableTh>Pekerjaan</TableTh>
<TableTh>Laki - Laki</TableTh>
<TableTh>Perempuan</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Hapus</TableTh>
</TableTr>
))}
</TableTbody>
</Table>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan 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>
{/* 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: 400, 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: '30%' }}>
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
</Box>
<Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Paper bg={colors['white-1']} p="md" radius="md" withBorder>
<Stack gap={"xs"}>
<Title pb={10} order={4}>
Grafik Demografi Pekerjaan
</Title>
{mounted && chartData.length > 0 ? (
<BarChart
h={450}
data={chartData}
dataKey="pekerjaan"
type="stacked"
series={[
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Paper>
</Box>
)}
<Box py={10}>
<Group justify='center'>
<Flex align="center" gap={10}>
<Box bg="#5082EE" w={20} h={20} />
<Text>Laki - Laki</Text>
</Flex>
<Flex align="center" gap={10}>
<Box bg="#6EDF9C" w={20} h={20} />
<Text>Perempuan</Text>
</Flex>
</Group>
</Box>
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus demografi pekerjaan ini?'
text="Apakah anda yakin ingin menghapus demografi pekerjaan ini?"
/>
</Box>
);