API & UI Menu Struktur bagian pegawai

This commit is contained in:
2025-07-07 17:14:44 +08:00
parent d86824a943
commit a2e25a3e3a
14 changed files with 1029 additions and 101 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { proxy } from "valtio";
import { z } from "zod";
import { toast } from "react-toastify";
@@ -210,12 +211,13 @@ const posisiOrganisasi = proxy({
const templatePegawai = z.object({
namaLengkap: z.string().min(1, "Nama wajib diisi"),
gelarAkademik: z.string().optional(),
imageId: z.string().optional(),
imageId: z.string().nullable().optional(),
tanggalMasuk: z.string().optional(), // ISO format
email: z.string().email("Email tidak valid").optional(),
telepon: z.string().optional(),
alamat: z.string().optional(),
posisiId: z.string().min(1, "Posisi wajib diisi"),
isActive: z.boolean().default(true),
});
const pegawaiDefaultForm = {
@@ -227,6 +229,7 @@ const templatePegawai = z.object({
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
const pegawai = proxy({
@@ -260,24 +263,36 @@ const templatePegawai = z.object({
}
},
},
findMany: {
data: null as Prisma.PegawaiGetPayload<{ include: { posisi: true } }>[] | null,
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean })[] | null,
async load() {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["find-many"].get();
if (res.status === 200) {
pegawai.findMany.data = res.data?.data ?? [];
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["pegawai"]["find-many"].get();
if (res.status === 200) {
pegawai.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
posisi: item.posisi || { id: '', nama: '' }, // Ensure posisi exists with required fields
isActive: item.isActive ?? true // Default to true if not provided
}));
} else {
console.error('Failed to load pegawai:', res.data?.message);
}
} catch (error) {
console.error('Error loading pegawai:', error);
pegawai.findMany.data = [];
}
},
},
findUnique: {
data: null as Prisma.PegawaiGetPayload<{ include: { posisi: true } }> | null,
data: null as (Prisma.PegawaiGetPayload<{ include: { posisi: true, image: true } }> & { isActive: boolean }) | null,
async load(id: string) {
const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/${id}`);
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`);
if (res.ok) {
const json = await res.json();
pegawai.findUnique.data = json.data ?? null;
pegawai.findUnique.data = json.data ? {
...json.data,
isActive: json.data.isActive ?? json.data.aktif ?? true // Fallback ke aktif:true jika tidak ada data
} : null;
} else {
pegawai.findUnique.data = null;
}
@@ -290,7 +305,7 @@ const templatePegawai = z.object({
if (!id) return toast.warn("ID tidak valid");
try {
pegawai.delete.loading = true;
const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/del/${id}`, {
const res = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/del/${id}`, {
method: "DELETE",
});
const json = await res.json();
@@ -309,65 +324,121 @@ const templatePegawai = z.object({
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
const res = await fetch(`/api/organisasi/pegawai/${id}`);
const json = await res.json();
if (res.ok && json.success) {
pegawai.edit.id = json.data.id;
pegawai.edit.form = {
namaLengkap: json.data.namaLengkap ?? "",
gelarAkademik: json.data.gelarAkademik ?? "",
imageId: json.data.imageId ?? "",
tanggalMasuk: json.data.tanggalMasuk?.slice(0, 10) ?? "",
email: json.data.email ?? "",
telepon: json.data.telepon ?? "",
alamat: json.data.alamat ?? "",
posisiId: json.data.posisiId,
};
} else {
toast.error("Gagal memuat data");
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = cek.error.issues.map(i => i.message).join("\n");
toast.error(err);
return;
}
try {
pegawai.edit.loading = true;
const res = await fetch(`/api/ekonomi/strukturorganisasi/pegawai/${pegawai.edit.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(pegawai.edit.form),
});
const json = await res.json();
if (res.ok) {
toast.success(json.message ?? "Berhasil update pegawai");
await pegawai.findMany.load();
} else {
toast.error(json.message ?? "Gagal update pegawai");
}
} catch (error) {
console.error("Gagal update:", error);
toast.error("Terjadi kesalahan saat update");
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
edit: {
id: "",
form: { ...pegawaiDefaultForm },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
gelarAkademik: data.gelarAkademik,
imageId: data.imageId,
tanggalMasuk: data.tanggalMasuk,
email: data.email,
telepon: data.telepon,
alamat: data.alamat,
posisiId: data.posisiId,
isActive: data.isActive,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading berita:", error);
toast.error(error instanceof Error ? error.message : "Gagal memuat data");
return null;
}
},
async submit() {
const cek = templatePegawai.safeParse(pegawai.edit.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
pegawai.edit.loading = true;
// Format tanggalMasuk to ISO string if it exists
const formattedTanggalMasuk = this.form.tanggalMasuk
? new Date(this.form.tanggalMasuk).toISOString()
: undefined;
const response = await fetch(`/api/ekonomi/struktur-organisasi/pegawai/${this.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: this.id,
namaLengkap: this.form.namaLengkap,
gelarAkademik: this.form.gelarAkademik,
imageId: this.form.imageId || null,
tanggalMasuk: formattedTanggalMasuk,
email: this.form.email,
telepon: this.form.telepon,
alamat: this.form.alamat,
posisiId: this.form.posisiId,
isActive: this.form.isActive,
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update pegawai");
await pegawai.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Terjadi kesalahan saat update pegawai");
return false;
} finally {
pegawai.edit.loading = false;
}
},
reset() {
pegawai.edit.id = "";
pegawai.edit.form = { ...pegawaiDefaultForm };
},
},
});
@@ -411,16 +482,15 @@ const hubunganOrganisasi = proxy({
} else {
return toast.error(res.data?.message || "Gagal menambahkan data");
}
} catch (error) {
console.error("Create Error:", error);
} catch (error) {
console.error("Gagal create:", error);
toast.error("Terjadi kesalahan saat menambahkan");
} finally {
hubunganOrganisasi.create.loading = false;
}
},
},
findMany: {
findMany: {
data: null as Array<{
id: string;
atasanId: string;
@@ -436,7 +506,7 @@ const hubunganOrganisasi = proxy({
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
@@ -450,7 +520,7 @@ const hubunganOrganisasi = proxy({
telepon: string | null;
alamat: string | null;
posisiId: string;
aktif: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
};
@@ -460,8 +530,18 @@ const hubunganOrganisasi = proxy({
try {
const res = await ApiFetch.api.ekonomi["struktur-organisasi"]["hubungan-organisasi"]["find-many"].get();
if (res.status === 200 && res.data?.success) {
hubunganOrganisasi.findMany.data = res.data.data || [];
if (res.status === 200) {
hubunganOrganisasi.findMany.data = (res.data?.data ?? []).map((item: any) => ({
...item,
atasan: item.atasan ? {
...item.atasan,
isActive: item.atasan.isActive ?? item.atasan.aktif ?? true
} : null,
bawahan: item.bawahan ? {
...item.bawahan,
isActive: item.bawahan.isActive ?? item.bawahan.aktif ?? true
} : null
}));
} else {
hubunganOrganisasi.findMany.data = [];
}
@@ -631,10 +711,10 @@ const hubunganOrganisasi = proxy({
},
});
const strukturorganisasiState = proxy({
posisiOrganisasi,
pegawai,
hubunganOrganisasi
})
const strukturorganisasiState = proxy({
posisiOrganisasi,
pegawai,
hubunganOrganisasi,
});
export default strukturorganisasiState;

View File

@@ -0,0 +1,276 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Image,
Paper,
Select,
Stack,
Text,
TextInput,
Title
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } 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';
interface PreviewImage {
file?: File;
preview: string;
}
interface PegawaiFormData {
namaLengkap: string;
gelarAkademik: string;
imageId: string | null;
tanggalMasuk: string;
email: string;
telepon: string;
alamat: string;
posisiId: string;
isActive: boolean;
}
export default function EditPegawai() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const [previewImage, setPreviewImage] = useState<PreviewImage | string | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const [formData, setFormData] = useState<PegawaiFormData>({
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
});
// Format date to YYYY-MM-DD for date input
const formatDateForInput = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toISOString().split('T')[0];
};
useEffect(() => {
strukturorganisasiState.posisiOrganisasi.findMany.load();
const loadPegawai = async () => {
try {
const data = await stateOrganisasi.edit.load(id);
if (data) {
setFormData({
namaLengkap: data.namaLengkap || "",
gelarAkademik: data.gelarAkademik || "",
imageId: data.imageId || "",
tanggalMasuk: data.tanggalMasuk || "",
email: data.email || "",
telepon: data.telepon || "",
alamat: data.alamat || "",
posisiId: data.posisiId || "",
isActive: data.isActive ?? true, // pakai nullish coalescing
});
if (data.image?.link) {
setPreviewImage(data.image.link);
} else {
setPreviewImage(null);
}
}
} catch (error) {
console.error("Error loading pegawai:", error);
toast.error(
error instanceof Error ? error.message : "Gagal mengambil data pegawai"
);
}
};
loadPegawai();
}, [id]);
const handleSubmit = async () => {
try {
if (!formData.namaLengkap.trim()) {
toast.error('Nama lengkap tidak boleh kosong');
return;
}
stateOrganisasi.edit.form = {
namaLengkap: formData.namaLengkap.trim(),
gelarAkademik: formData.gelarAkademik.trim(),
imageId: formData.imageId ? formData.imageId.trim() : "",
tanggalMasuk: formData.tanggalMasuk.trim(),
email: formData.email.trim(),
telepon: formData.telepon.trim(),
alamat: formData.alamat.trim(),
posisiId: formData.posisiId.trim(),
isActive: formData.isActive,
};
if (id && !stateOrganisasi.edit.id) {
stateOrganisasi.edit.id = id;
}
const success = await stateOrganisasi.edit.submit();
if (success) {
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
}
} catch (error) {
console.error("Error updating pegawai:", error);
toast.error(error instanceof Error ? error.message : "Gagal memperbarui data pegawai");
}
};
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 Data Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={formData.namaLengkap}
onChange={(e) => setFormData({ ...formData, namaLengkap: e.target.value })}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={formData.gelarAkademik}
onChange={(e) => setFormData({ ...formData, gelarAkademik: e.target.value })}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Image
src={typeof previewImage === 'string' ? previewImage : previewImage?.preview}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
)}
</Box>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={formatDateForInput(formData.tanggalMasuk)}
onChange={(e) => setFormData({ ...formData, tanggalMasuk: e.target.value })}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={formData.email}
onChange={(e) => (formData.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={formData.telepon}
onChange={(e) => (formData.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={formData.alamat}
onChange={(e) => (formData.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={
strukturorganisasiState.posisiOrganisasi.findMany.data?.map((p) => ({
value: p.id, // harus string
label: p.nama,
})) || []
}
value={formData.posisiId}
onChange={(value) => {
if (value !== null) {
setFormData({ ...formData, posisiId: value }); // value harus string
}
}}
/>
<Select
label="Status Pegawai"
placeholder="Pilih status"
data={[
{ value: 'true', label: 'Aktif' },
{ value: 'false', label: 'Tidak Aktif' },
]}
value={String(formData.isActive)}
onChange={(val) => {
if (val !== null) {
setFormData({ ...formData, isActive: val === 'true' });
}
}}
/>
<Group>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box >
);
}

View File

@@ -0,0 +1,148 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import { Box, Button, Flex, Image, 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 DetailPegawai() {
const statePegawai = useProxy(strukturorganisasiState.pegawai)
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const params = useParams()
const router = useRouter();
useShallowEffect(() => {
statePegawai.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
statePegawai.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai")
}
}
if (!statePegawai.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
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 Pegawai</Text>
<Paper bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fz={"lg"} fw={"bold"}>Nama Lengkap</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Gelar Akademik</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.gelarAkademik}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Image</Text>
{statePegawai.findUnique.data?.image?.link ? (
<Image src={statePegawai.findUnique.data?.image?.link} alt='' />
) : (
<Text fz={"md"} c="dimmed">Tidak ada gambar</Text>
)}
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Tanggal Masuk</Text>
<Text fz={"lg"}>
{statePegawai.findUnique.data?.tanggalMasuk
? new Date(statePegawai.findUnique.data.tanggalMasuk).toLocaleDateString()
: "-"}
</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Email</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Telepon</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.telepon}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Alamat</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.alamat}</Text>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Posisi</Text>
<Stack gap={4}>
{statePegawai.findUnique.data?.posisi ? (
<Text fz={"lg"}>
{statePegawai.findUnique.data.posisi.nama}
</Text>
) : (
<Text fz={"lg"} c="dimmed">
Tidak ada posisi
</Text>
)}
</Stack>
</Box>
<Box>
<Text fz={"lg"} fw={"bold"}>Aktif</Text>
<Text fz={"lg"}>{statePegawai.findUnique.data?.isActive ? "Ya" : "Tidak"}</Text>
</Box>
<Box>
<Flex gap={"xs"}>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
setSelectedId(statePegawai.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={!statePegawai.findUnique.data}
color="red">
<IconX size={20} />
</Button>
<Button
onClick={() => {
if (statePegawai.findUnique.data) {
router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${statePegawai.findUnique.data.id}/edit`);
}
}}
disabled={!statePegawai.findUnique.data}
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 produk ini?"
/>
</Box>
);
}
export default DetailPegawai;

View File

@@ -0,0 +1,200 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import strukturorganisasiState from '@/app/admin/(dashboard)/_state/ekonomi/struktur-organisasi/struktur-organisasi';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePegawai() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const stateOrganisasi = useProxy(strukturorganisasiState)
useEffect(() => {
stateOrganisasi.posisiOrganisasi.findMany.load();
resetForm();
}, []);
const resetForm = () => {
stateOrganisasi.pegawai.create.form = {
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
isActive: true,
};
};
const handleSubmit = async () => {
if (!previewImage) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
try {
// Upload gambar dulu
const res = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file,
name: previewImage.file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
}
// Set status aktif secara otomatis
stateOrganisasi.pegawai.create.form.isActive = true;
// Simpan ID gambar ke form
stateOrganisasi.pegawai.create.form.imageId = uploaded.id;
// Submit form
await stateOrganisasi.pegawai.create.submit();
// Reset form dan redirect
resetForm();
toast.success("Data pegawai berhasil ditambahkan");
router.push("/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai");
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
}
};
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors["white-1"]} p={"md"} w={{ base: "100%", md: "50%" }}>
<Stack gap={"xs"}>
<Title order={3}>Create Pegawai</Title>
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
value={stateOrganisasi.pegawai.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.pegawai.create.form.namaLengkap = e.currentTarget.value)}
/>
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
value={stateOrganisasi.pegawai.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.pegawai.create.form.gelarAkademik = e.currentTarget.value)}
/>
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box >
<Dropzone
onDrop={(files) => {
const file = files[0]; // Hanya ambil file pertama
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
>
<Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag images here or click to select files
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Attach as many files as you like, each file should not exceed 5mb
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Image
src={previewImage.preview}
alt="Preview"
width={280}
height={180}
fit="cover"
radius="sm"
mt="md"
/>
)}
</Box>
</Box>
<TextInput
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
value={stateOrganisasi.pegawai.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.pegawai.create.form.tanggalMasuk = e.currentTarget.value)}
/>
<TextInput
label="Email"
placeholder="Contoh: email@example.com"
value={stateOrganisasi.pegawai.create.form.email}
onChange={(e) => (stateOrganisasi.pegawai.create.form.email = e.currentTarget.value)}
/>
<TextInput
label="Telepon"
placeholder="Contoh: 08123456789"
value={stateOrganisasi.pegawai.create.form.telepon}
onChange={(e) => (stateOrganisasi.pegawai.create.form.telepon = e.currentTarget.value)}
/>
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
value={stateOrganisasi.pegawai.create.form.alamat}
onChange={(e) => (stateOrganisasi.pegawai.create.form.alamat = e.currentTarget.value)}
/>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateOrganisasi.posisiOrganisasi.findMany.data?.map(p => ({
value: p.id,
label: p.nama
})) || []}
value={stateOrganisasi.pegawai.create.form.posisiId}
onChange={(value) => {
if (value) stateOrganisasi.pegawai.create.form.posisiId = value;
}}
searchable
/>
<Button
onClick={handleSubmit}
color="blue"
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
);
}
export default CreatePegawai;

View File

@@ -1,11 +1,133 @@
import React from 'react';
'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 { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import JudulList from '../../../_com/judulList';
import strukturorganisasiState from '../../../_state/ekonomi/struktur-organisasi/struktur-organisasi';
function Page() {
function Pegawai() {
return (
<div>
Page
</div>
<Box>
<HeaderSearch
title='Pegawai'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>
<ListPegawai />
</Box>
);
}
export default Page;
function ListPegawai() {
const stateOrganisasi = useProxy(strukturorganisasiState.pegawai);
const router = useRouter();
useShallowEffect(() => {
const loadData = async () => {
console.log('1. Starting to load pegawai data...');
try {
// Clear existing data to ensure we see the loading state
stateOrganisasi.findMany.data = [];
// Load new data
await stateOrganisasi.findMany.load();
// Log the raw response and state
console.log('2. Raw API response:', stateOrganisasi.findMany.data);
// Type guard to ensure data is an array
const data = stateOrganisasi.findMany.data || [];
console.log(`3. Loaded ${data.length} pegawai records`);
if (data.length > 0) {
console.log('4. First record sample:', data[0]);
}
} catch (error) {
console.error('Error loading pegawai data:', error);
stateOrganisasi.findMany.data = [];
}
};
loadData();
// Cleanup function
return () => {
console.log('Cleanup: Unmounting component');
};
}, []);
// Log render cycle
console.log('Rendering with data:', stateOrganisasi.findMany.data);
// Handle loading state
if (stateOrganisasi.findMany.data === null) {
console.log('Showing loading state');
return (
<Stack py={10}>
<Skeleton height={300} />
</Stack>
);
}
// Check if data is an empty array
const data = stateOrganisasi.findMany.data || [];
if (data.length === 0) {
console.log('No data available to display');
return (
<Box py={10}>
<Paper p="md" ta="center">
<p>Tidak ada data pegawai yang tersedia</p>
</Paper>
</Box>
);
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Pegawai'
href='/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>
<TableTh>Gelar Akademik</TableTh>
<TableTh>Telepon</TableTh>
<TableTh>Posisi</TableTh>
<TableTh>Aktif</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{(() => {
console.log('Rendering table with items:', stateOrganisasi.findMany.data);
return null;
})()}
{(stateOrganisasi.findMany.data || []).map((item) => (
<TableTr key={item.id}>
<TableTd>{item.namaLengkap}</TableTd>
<TableTd>{item.gelarAkademik}</TableTd>
<TableTd>{item.telepon}</TableTd>
<TableTd>{item.posisi?.nama}</TableTd>
<TableTd>
<Badge color={item.isActive ? "green" : "red"}>{item.isActive ? "Aktif" : "Tidak Aktif"}</Badge>
</TableTd>
<TableTd>
<Button bg={"green"} onClick={() => router.push(`/admin/ekonomi/struktur-organisasi-dan-sk-pengurus-bumdesa/pegawai/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
</Box>
);
}
export default Pegawai;

View File

@@ -11,15 +11,23 @@ type FormCreatePegawai = {
telepon?: string;
alamat?: string;
posisiId: string;
isActive?: boolean;
};
export default async function pegawaiCreate(context: Context) {
const body = await context.body as FormCreatePegawai;
if (!body || !body.namaLengkap || !body.posisiId) {
if (!body) {
return {
success: false,
message: "namaLengkap dan posisiId wajib diisi",
message: "Data tidak valid",
};
}
if (!body.namaLengkap || !body.posisiId || !body.imageId) {
return {
success: false,
message: "Nama lengkap, posisi, dan gambar wajib diisi",
};
}
@@ -34,7 +42,8 @@ export default async function pegawaiCreate(context: Context) {
telepon: body.telepon,
alamat: body.alamat,
posisiId: body.posisiId,
// aktif, createdAt, updatedAt otomatis by Prisma default
isActive: body.isActive !== false // default true kalau tidak dikirim
},
});
@@ -44,10 +53,19 @@ export default async function pegawaiCreate(context: Context) {
data: pegawai,
};
} catch (error: any) {
console.error("Error creating pegawai:", error);
if (error.code === "P2002") {
// Cek field mana yang duplicate
const target = error.meta?.target?.[0];
let message = "Data sudah ada";
if (target === 'email') message = "Email sudah digunakan";
else if (target === 'telepon') message = "Nomor telepon sudah digunakan";
return {
success: false,
message: "Email sudah digunakan",
message,
};
}

View File

@@ -16,7 +16,7 @@ export default async function pegawaiDelete(context: Context) {
const deleted = await prisma.pegawai.update({
where: { id },
data: {
aktif: false, // soft delete
isActive: false, // soft delete
updatedAt: new Date(),
},
});

View File

@@ -4,7 +4,7 @@ import prisma from "@/lib/prisma";
export default async function pegawaiFindMany() {
try {
const pegawaiList = await prisma.pegawai.findMany({
where: { aktif: true }, // hanya yang aktif
where: { isActive: true }, // Using the correct field name from Prisma model
orderBy: { createdAt: "desc" },
include: {
posisi: true,

View File

@@ -57,6 +57,6 @@ const Pegawai = new Elysia({
)
// ✅ Delete
.delete("/:id", pegawaiDelete);
.delete("/del/:id", pegawaiDelete);
export default Pegawai;

View File

@@ -12,7 +12,7 @@ type FormUpdatePegawai = {
telepon?: string;
alamat?: string;
posisiId?: string;
aktif?: boolean;
isActive?: boolean;
};
export default async function pegawaiUpdate(context: Context) {
@@ -37,9 +37,13 @@ export default async function pegawaiUpdate(context: Context) {
telepon: body.telepon,
alamat: body.alamat,
posisiId: body.posisiId,
aktif: body.aktif,
isActive: body.isActive === true, // Konversi ke boolean
updatedAt: new Date(),
},
include: {
image: true,
posisi: true,
}
});
return {