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

@@ -8,7 +8,7 @@
"telepon": "081234567891",
"alamat": "Jl. Raya Desa No. 1",
"posisiId": "kepala_desa",
"aktif": true
"isActive": true
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
@@ -19,6 +19,6 @@
"telepon": "081234567892",
"alamat": "Jl. Raya Desa No. 2",
"posisiId": "sekretaris_desa",
"aktif": true
"isActive": true
}
]

View File

@@ -0,0 +1,78 @@
-- CreateTable
CREATE TABLE "posisi_organisasi" (
"id" VARCHAR(50) NOT NULL,
"nama" VARCHAR(100) NOT NULL,
"deskripsi" TEXT,
"hierarki" INTEGER NOT NULL,
CONSTRAINT "posisi_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "pegawai" (
"id" UUID NOT NULL,
"namaLengkap" VARCHAR(255) NOT NULL,
"gelarAkademik" VARCHAR(100),
"imageId" TEXT,
"tanggalMasuk" DATE,
"email" VARCHAR(255),
"telepon" VARCHAR(20),
"alamat" TEXT,
"posisiId" VARCHAR(50) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "pegawai_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "hubungan_organisasi" (
"id" UUID NOT NULL,
"atasanId" UUID NOT NULL,
"bawahanId" UUID NOT NULL,
"tipe" VARCHAR(50),
CONSTRAINT "hubungan_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "struktur_organisasi" (
"id" TEXT NOT NULL,
"posisiOrganisasiId" VARCHAR(50) NOT NULL,
"pegawaiId" UUID NOT NULL,
"hubunganOrganisasiId" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"isActive" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "struktur_organisasi_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "pegawai_email_key" ON "pegawai"("email");
-- CreateIndex
CREATE UNIQUE INDEX "hubungan_organisasi_atasanId_bawahanId_key" ON "hubungan_organisasi"("atasanId", "bawahanId");
-- AddForeignKey
ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pegawai" ADD CONSTRAINT "pegawai_posisiId_fkey" FOREIGN KEY ("posisiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_atasanId_fkey" FOREIGN KEY ("atasanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "hubungan_organisasi" ADD CONSTRAINT "hubungan_organisasi_bawahanId_fkey" FOREIGN KEY ("bawahanId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_posisiOrganisasiId_fkey" FOREIGN KEY ("posisiOrganisasiId") REFERENCES "posisi_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_pegawaiId_fkey" FOREIGN KEY ("pegawaiId") REFERENCES "pegawai"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "struktur_organisasi" ADD CONSTRAINT "struktur_organisasi_hubunganOrganisasiId_fkey" FOREIGN KEY ("hubunganOrganisasiId") REFERENCES "hubungan_organisasi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -1133,7 +1133,7 @@ model Pegawai {
telepon String? @db.VarChar(20)
alamat String? @db.Text
posisiId String @db.VarChar(50)
aktif Boolean @default(true)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -395,6 +395,7 @@ import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
create: {
id: p.id,
@@ -405,6 +406,7 @@ import pegawai from "./data/ekonomi/struktur-organisasi/pegawai.json";
telepon: p.telepon,
alamat: p.alamat,
posisiId: p.posisiId,
isActive: p.isActive,
},
});
}

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 {