UI & API Menu PPID, Submenu Struktur PPID
This commit is contained in:
@@ -1,225 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button, Center, FileInput, Group, Image, Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconAlertCircle, IconArrowBack, IconImageInPicture } from '@tabler/icons-react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import { toast } from 'react-toastify';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
|
||||
function EditStrukturPPID() {
|
||||
const strukturPPID = useProxy(stateStrukturPPID);
|
||||
const params = useParams();
|
||||
const router = useRouter()
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const id = params?.id as string;
|
||||
if (!id) {
|
||||
toast.error("ID tidak valid");
|
||||
router.push("/admin/ppid/struktur-ppid");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profileData = await strukturPPID.loadForEdit(id);
|
||||
|
||||
if (profileData && profileData.image?.link) {
|
||||
setPreviewImage(profileData.image.link);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading profile:", error);
|
||||
toast.error("Gagal memuat data profile");
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
|
||||
return () => {
|
||||
strukturPPID.editStruktur.reset(); // cleanup form
|
||||
};
|
||||
}, [params?.id, router]);
|
||||
|
||||
const handleFieldChange = (field: string, value: string) => {
|
||||
strukturPPID.editStruktur.updateField(field as any, value);
|
||||
};
|
||||
|
||||
const handleFileChange = (newFile: File | null) => {
|
||||
if (!newFile) {
|
||||
setFile(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(newFile);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setPreviewImage(event.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(newFile);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting || !strukturPPID.editStruktur.form.name.trim()) {
|
||||
toast.error("Nama wajib diisi");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Upload file jika ada
|
||||
if (file) {
|
||||
const uploadResponse = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = uploadResponse.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
toast.error("Gagal upload gambar");
|
||||
return;
|
||||
}
|
||||
|
||||
strukturPPID.editStruktur.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
// Submit form
|
||||
const success = await strukturPPID.editStruktur.submit();
|
||||
|
||||
if (success) {
|
||||
toast.success("Berhasil menyimpan perubahan");
|
||||
router.push("/admin/ppid/struktur-ppid");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
toast.error("Gagal menyimpan profile");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
if (strukturPPID.struktur.loading) {
|
||||
return (
|
||||
<Box>
|
||||
<Center h={400}>
|
||||
<Text>Memuat data struktur...</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (strukturPPID.struktur.error) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
<Alert icon={<IconAlertCircle size={16} />} color="red">
|
||||
<Text fw="bold">Error</Text>
|
||||
<Text>{strukturPPID.struktur.error}</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// No data state
|
||||
if (!strukturPPID.struktur.data) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" onClick={handleBack}>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
<Alert icon={<IconAlertCircle size={16} />} color="yellow">
|
||||
<Text fw="bold">Data tidak ditemukan</Text>
|
||||
<Text>Profile PPID tidak dapat ditemukan</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack gap={'xs'}>
|
||||
<Box>
|
||||
<Button
|
||||
variant={'subtle'}
|
||||
onClick={handleBack}
|
||||
>
|
||||
<IconArrowBack color={colors['blue-button']} size={20} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box>
|
||||
<Paper bg={colors['white-1']} p={'md'} radius={10} w={{ base: '100%', md: '50%' }}>
|
||||
<Stack gap={'xs'}>
|
||||
<Title order={3}>Edit Struktur PPID</Title>
|
||||
<TextInput
|
||||
value={strukturPPID.editStruktur.form.name}
|
||||
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
|
||||
label={<Text fz="sm" fw="bold">Judul</Text>}
|
||||
placeholder="Masukkan judul"
|
||||
/>
|
||||
{/* File Upload */}
|
||||
<FileInput
|
||||
label={<Text fz="sm" fw="bold">Upload Gambar Baru (Opsional)</Text>}
|
||||
value={file}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
/>
|
||||
|
||||
{/* Preview Gambar */}
|
||||
<Box>
|
||||
<Text fz="sm" fw="bold" mb="xs">Preview Gambar</Text>
|
||||
{previewImage ? (
|
||||
<Image alt="Profile preview" src={previewImage} w={200} h={200} fit="cover" radius="md" />
|
||||
) : (
|
||||
<Center w={200} h={200} bg="gray.2">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconImageInPicture size={48} color="gray" />
|
||||
<Text size="sm" c="gray">Tidak ada gambar</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || strukturPPID.editStruktur.loading}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditStrukturPPID;
|
||||
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
function LayoutTabs({ children }: { children: React.ReactNode }) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const tabs = [
|
||||
{
|
||||
label: "Pegawai",
|
||||
value: "pegawai",
|
||||
href: "/admin/ppid/struktur-ppid/pegawai"
|
||||
},
|
||||
{
|
||||
label: "Posisi Organisasi",
|
||||
value: "posisiorganisasi",
|
||||
href: "/admin/ppid/struktur-ppid/posisi-organisasi"
|
||||
},
|
||||
{
|
||||
label: "Struktur Organisasi",
|
||||
value: "strukturorganisasi",
|
||||
href: "/admin/ppid/struktur-ppid/struktur-organisasi"
|
||||
}
|
||||
];
|
||||
const curentTab = tabs.find(tab => tab.href === pathname)
|
||||
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
|
||||
|
||||
const handleTabChange = (value: string | null) => {
|
||||
const tab = tabs.find(t => t.value === value)
|
||||
if (tab) {
|
||||
router.push(tab.href)
|
||||
}
|
||||
setActiveTab(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const match = tabs.find(tab => tab.href === pathname)
|
||||
if (match) {
|
||||
setActiveTab(match.value)
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={3}>Struktur PPID Desa Darmasaba</Title>
|
||||
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
|
||||
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((e, i) => (
|
||||
<TabsPanel key={i} value={e.value}>
|
||||
{/* Konten dummy, bisa diganti tergantung routing */}
|
||||
<></>
|
||||
</TabsPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default LayoutTabs;
|
||||
12
src/app/admin/(dashboard)/ppid/struktur-ppid/layout.tsx
Normal file
12
src/app/admin/(dashboard)/ppid/struktur-ppid/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import LayoutTabs from "./_lib/layoutTabs"
|
||||
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<LayoutTabs>
|
||||
{children}
|
||||
</LayoutTabs>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateStrukturPPID from '../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const strukturPPID = useProxy(stateStrukturPPID)
|
||||
useShallowEffect(() => {
|
||||
strukturPPID.struktur.load("1")
|
||||
}, [])
|
||||
|
||||
if (!strukturPPID.struktur.data) {
|
||||
return <Stack>
|
||||
<Skeleton radius={10} h={800} />
|
||||
</Stack>
|
||||
}
|
||||
|
||||
const dataArray = Array.isArray(strukturPPID.struktur.data)
|
||||
? strukturPPID.struktur.data
|
||||
: [strukturPPID.struktur.data];
|
||||
|
||||
return (
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack gap={"xs"}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 11 }}>
|
||||
<Title order={3}>Preview Struktur PPID</Title>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 1 }}>
|
||||
<Button onClick={() => router.push(`/admin/ppid/struktur-ppid/${strukturPPID.struktur.data?.id}`)} bg={colors['blue-button']}>
|
||||
<IconEdit size={16} />
|
||||
</Button>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
{dataArray.map((item) => (
|
||||
<Paper key={item.id} p={"xl"} bg={colors['BG-trans']}>
|
||||
<Box>
|
||||
<Text fw={"bold"} fz={"h4"}>{item.name}</Text>
|
||||
<Image
|
||||
w={{ base: "100%", md: "100%" }}
|
||||
src={item.image?.link}
|
||||
alt=''
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/struktur_ppid.png";
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,275 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
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 { useParams, useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
|
||||
export default function EditPegawaiPPID() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||
const [formData, setFormData] = useState({
|
||||
namaLengkap: stateOrganisasi.edit.form.namaLengkap,
|
||||
gelarAkademik: stateOrganisasi.edit.form.gelarAkademik,
|
||||
imageId: stateOrganisasi.edit.form.imageId,
|
||||
tanggalMasuk: stateOrganisasi.edit.form.tanggalMasuk,
|
||||
email: stateOrganisasi.edit.form.email,
|
||||
telepon: stateOrganisasi.edit.form.telepon,
|
||||
alamat: stateOrganisasi.edit.form.alamat,
|
||||
posisiId: stateOrganisasi.edit.form.posisiId,
|
||||
isActive: stateOrganisasi.edit.form.isActive,
|
||||
});
|
||||
|
||||
|
||||
// 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(() => {
|
||||
stateStrukturPPID.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 || "",
|
||||
tanggalMasuk: formData.tanggalMasuk.trim(),
|
||||
email: formData.email.trim(),
|
||||
telepon: formData.telepon.trim(),
|
||||
alamat: formData.alamat.trim(),
|
||||
posisiId: formData.posisiId.trim(),
|
||||
isActive: formData.isActive,
|
||||
};
|
||||
|
||||
if (file) {
|
||||
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||
const uploaded = res.data?.data;
|
||||
|
||||
if (!uploaded?.id) {
|
||||
return toast.error("Gagal upload gambar");
|
||||
}
|
||||
|
||||
// Update imageId in global state
|
||||
stateOrganisasi.edit.form.imageId = uploaded.id;
|
||||
}
|
||||
|
||||
if (id && !stateOrganisasi.edit.id) {
|
||||
stateOrganisasi.edit.id = id;
|
||||
}
|
||||
|
||||
const success = await stateOrganisasi.edit.submit();
|
||||
|
||||
if (success) {
|
||||
router.push("/admin/ppid/struktur-ppid/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 PPID</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) {
|
||||
setFile(file);
|
||||
setPreviewImage(URL.createObjectURL(file)); // Buat preview
|
||||
}
|
||||
}}
|
||||
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}
|
||||
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={
|
||||
stateStrukturPPID.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"
|
||||
data={[
|
||||
{ value: 'true', label: 'Aktif' },
|
||||
{ value: 'false', label: 'Tidak Aktif' },
|
||||
]}
|
||||
value={String(formData.isActive)} // 'true' atau 'false'
|
||||
onChange={(val) => {
|
||||
setFormData({ ...formData, isActive: val === 'true' });
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
color="blue"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box >
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
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(stateStrukturPPID.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/ppid/struktur-ppid/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 PPID</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/ppid/struktur-ppid/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 pegawai ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default DetailPegawai;
|
||||
@@ -0,0 +1,200 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
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 CreatePegawaiPPID() {
|
||||
const router = useRouter();
|
||||
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||
useEffect(() => {
|
||||
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||
resetForm();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateOrganisasi.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.create.form.isActive = true;
|
||||
|
||||
// Simpan ID gambar ke form
|
||||
stateOrganisasi.create.form.imageId = uploaded.id;
|
||||
|
||||
// Submit form
|
||||
await stateOrganisasi.create.submit();
|
||||
|
||||
|
||||
// Reset form dan redirect
|
||||
resetForm();
|
||||
toast.success("Data pegawai berhasil ditambahkan");
|
||||
router.push("/admin/ppid/struktur-ppid/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.create.form.namaLengkap}
|
||||
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Gelar Akademik"
|
||||
placeholder="Contoh: S.Kom"
|
||||
value={stateOrganisasi.create.form.gelarAkademik}
|
||||
onChange={(e) => (stateOrganisasi.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.create.form.tanggalMasuk}
|
||||
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="Contoh: email@example.com"
|
||||
value={stateOrganisasi.create.form.email}
|
||||
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Telepon"
|
||||
placeholder="Contoh: 08123456789"
|
||||
value={stateOrganisasi.create.form.telepon}
|
||||
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
|
||||
/>
|
||||
<TextInput
|
||||
label="Alamat"
|
||||
placeholder="Contoh: Jl. Contoh No. 1"
|
||||
value={stateOrganisasi.create.form.alamat}
|
||||
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
|
||||
/>
|
||||
<Select
|
||||
label="Posisi"
|
||||
placeholder="Pilih posisi"
|
||||
data={stateStrukturPPID.posisiOrganisasi.findMany.data?.map(p => ({
|
||||
value: p.id,
|
||||
label: p.nama
|
||||
})) || []}
|
||||
value={stateOrganisasi.create.form.posisiId}
|
||||
onChange={(value) => {
|
||||
if (value) stateOrganisasi.create.form.posisiId = value;
|
||||
}}
|
||||
searchable
|
||||
/>
|
||||
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
color="blue"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePegawaiPPID;
|
||||
178
src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/page.tsx
Normal file
178
src/app/admin/(dashboard)/ppid/struktur-ppid/pegawai/page.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
|
||||
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
|
||||
function PegawaiPPID() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Pegawai PPID'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPegawaiPPID search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPegawaiPPID({ search }: { search: string }) {
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateOrganisasi.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10);
|
||||
}, [page]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.namaLengkap?.toLowerCase().includes(keyword) ||
|
||||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
|
||||
item.telepon?.toLowerCase().includes(keyword) ||
|
||||
item.posisi?.nama?.toLowerCase().includes(keyword)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||
}, [data, search]);
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton height={300} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Pegawai'
|
||||
href='/admin/ppid/struktur-ppid/pegawai/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<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>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'} h={{base: 770, md: 650}}>
|
||||
<JudulList
|
||||
title='List Pegawai'
|
||||
href='/admin/ppid/struktur-ppid/pegawai/create'
|
||||
/>
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<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;
|
||||
})()}
|
||||
{([...filteredData]
|
||||
.sort((a, b) => {
|
||||
if (a.isActive === b.isActive) {
|
||||
return a.namaLengkap.localeCompare(b.namaLengkap); // kalau status sama, urut nama
|
||||
}
|
||||
return Number(b.isActive) - Number(a.isActive); // aktif duluan
|
||||
}) // Aktif di atas
|
||||
).map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.namaLengkap}</TableTd>
|
||||
<TableTd>{item.gelarAkademik}</TableTd>
|
||||
<TableTd>{item.telepon}</TableTd>
|
||||
<TableTd>{item.posisi?.nama}</TableTd>
|
||||
<TableTd>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Box visibleFrom="sm">
|
||||
<Badge color={item.isActive ? "green" : "red"}>
|
||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box hiddenFrom="sm">
|
||||
{item.isActive ? (
|
||||
<ThemeIcon color="green" variant="light" size="sm">
|
||||
<IconCheck size={16} />
|
||||
</ThemeIcon>
|
||||
) : (
|
||||
<ThemeIcon color="red" variant="light" size="sm">
|
||||
<IconX size={16} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button bg={"green"} onClick={() => router.push(`/admin/ppid/struktur-ppid/pegawai/${item.id}`)}>
|
||||
<IconDeviceImacCog size={25} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => {
|
||||
load(newPage, 10);
|
||||
window.scrollTo(0, 0);
|
||||
}}
|
||||
total={totalPages}
|
||||
mt="md"
|
||||
mb="md"
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PegawaiPPID;
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } 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';
|
||||
|
||||
function EditPosisiOrganisasiPPID() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const id = params?.id as string;
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
nama: "",
|
||||
deskripsi: "",
|
||||
hierarki: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadPosisiOrganisasi = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const data = await stateOrganisasi.edit.load(id);
|
||||
|
||||
if (data) {
|
||||
// pastikan id-nya masuk ke state edit
|
||||
stateOrganisasi.edit.id = id;
|
||||
setFormData({
|
||||
nama: data.nama || '',
|
||||
deskripsi: data.deskripsi || '',
|
||||
hierarki: data.hierarki || 0,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading posisi organisasi:", error);
|
||||
toast.error("Gagal memuat data posisi organisasi");
|
||||
}
|
||||
};
|
||||
|
||||
loadPosisiOrganisasi();
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (!formData.nama.trim()) {
|
||||
toast.error('Nama posisi organisasi tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
|
||||
stateOrganisasi.edit.form = {
|
||||
nama: formData.nama.trim(),
|
||||
deskripsi: formData.deskripsi.trim(),
|
||||
hierarki: formData.hierarki,
|
||||
};
|
||||
|
||||
// Safety check tambahan: pastikan ID tidak kosong
|
||||
if (!stateOrganisasi.edit.id) {
|
||||
stateOrganisasi.edit.id = id; // fallback
|
||||
}
|
||||
|
||||
const success = await stateOrganisasi.edit.update();
|
||||
|
||||
if (success) {
|
||||
router.push("/admin/ppid/struktur-ppid/posisi-organisasi");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating posisi organisasi:", error);
|
||||
// toast akan ditampilkan dari fungsi update
|
||||
}
|
||||
};
|
||||
|
||||
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 Posisi Organisasi PPID</Title>
|
||||
<TextInput
|
||||
value={formData.nama}
|
||||
onChange={(e) => setFormData({ ...formData, nama: e.target.value })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Nama Posisi Organisasi</Text>}
|
||||
placeholder='Masukkan nama posisi organisasi'
|
||||
/>
|
||||
<EditEditor
|
||||
value={formData.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
setFormData({ ...formData, deskripsi: htmlContent });
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
value={formData.hierarki}
|
||||
onChange={(e) => setFormData({ ...formData, hierarki: parseInt(e.target.value) })}
|
||||
label={<Text fw={"bold"} fz={"sm"}>Hierarki</Text>}
|
||||
placeholder='Masukkan hierarki'
|
||||
/>
|
||||
<Group>
|
||||
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditPosisiOrganisasiPPID;
|
||||
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
|
||||
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { IconArrowBack } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function CreatePosisiOrganisasiPPID() {
|
||||
const router = useRouter();
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
|
||||
useEffect(() => {
|
||||
stateOrganisasi.findMany.load();
|
||||
}, []);
|
||||
|
||||
const resetForm = () => {
|
||||
stateOrganisasi.create.form = {
|
||||
nama: "",
|
||||
deskripsi: "",
|
||||
hierarki: 0, // Initialize as 0 to allow any number input
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await stateOrganisasi.create.submit();
|
||||
resetForm();
|
||||
router.push("/admin/ppid/struktur-ppid/posisi-organisasi")
|
||||
};
|
||||
|
||||
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 Posisi Organisasi PPID</Title>
|
||||
<TextInput
|
||||
label="Nama Posisi"
|
||||
placeholder="Contoh: Kepala Desa"
|
||||
value={stateOrganisasi.create.form.nama}
|
||||
onChange={(e) => (stateOrganisasi.create.form.nama = e.currentTarget.value)}
|
||||
/>
|
||||
<CreateEditor
|
||||
value={stateOrganisasi.create.form.deskripsi}
|
||||
onChange={(htmlContent) => {
|
||||
stateOrganisasi.create.form.deskripsi = htmlContent;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Hierarki"
|
||||
type="number"
|
||||
placeholder="Contoh: 1"
|
||||
value={stateOrganisasi.create.form.hierarki}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
if (!isNaN(value)) {
|
||||
stateOrganisasi.create.form.hierarki = value;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
color="blue"
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePosisiOrganisasiPPID;
|
||||
@@ -0,0 +1,134 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconEdit, 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 stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
|
||||
function PosisiOrganisasiPPID() {
|
||||
const [search, setSearch] = useState("");
|
||||
return (
|
||||
<Box>
|
||||
<HeaderSearch
|
||||
title='Posisi Organisasi PPID'
|
||||
placeholder='pencarian'
|
||||
searchIcon={<IconSearch size={20} />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<ListPosisiOrganisasiPPID search={search} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi)
|
||||
const router = useRouter();
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
|
||||
const handleHapus = async () => {
|
||||
if (selectedId) {
|
||||
await stateOrganisasi.delete.byId(selectedId);
|
||||
setModalHapus(false)
|
||||
setSelectedId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.nama?.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi?.toLowerCase().includes(keyword) ||
|
||||
item.hierarki?.toString().toLowerCase().includes(keyword)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => a.hierarki - b.hierarki);
|
||||
|
||||
if (!stateOrganisasi.findMany.data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<JudulList
|
||||
title='List Posisi Organisasi PPID'
|
||||
href='/admin/ppid/struktur-ppid/posisi-organisasi/create'
|
||||
/>
|
||||
<Box style={{ overflowY: 'auto', maxHeight: '400px' }}>
|
||||
<Table striped withTableBorder withRowBorders>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh>Nama Posisi</TableTh>
|
||||
<TableTh>Deskripsi</TableTh>
|
||||
<TableTh>Hierarki</TableTh>
|
||||
<TableTh>Edit</TableTh>
|
||||
<TableTh>Hapus</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody >
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>
|
||||
<Text truncate dangerouslySetInnerHTML={{ __html: item.deskripsi ?? "" }} />
|
||||
</TableTd>
|
||||
<TableTd>{item.hierarki}</TableTd>
|
||||
<TableTd>
|
||||
<Button color="green"
|
||||
onClick={() => {
|
||||
if (item) {
|
||||
router.push(`/admin/ppid/struktur-ppid/posisi-organisasi/${item.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Button color="red"
|
||||
onClick={() => {
|
||||
if (item) {
|
||||
setSelectedId(item.id);
|
||||
setModalHapus(true);
|
||||
}
|
||||
}}
|
||||
disabled={!item}
|
||||
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</Button>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
onClose={() => setModalHapus(false)}
|
||||
onConfirm={handleHapus}
|
||||
text="Apakah anda yakin ingin menghapus posisi organisasi PPID ini?"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default PosisiOrganisasiPPID;
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Image, Paper, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import { OrganizationChart } from 'primereact/organizationchart';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import stateStrukturPPID from '../../../_state/ppid/struktur_ppid/struktur_PPID';
|
||||
|
||||
function StrukturOrganisasiPPID() {
|
||||
return (
|
||||
<Box >
|
||||
<ListStrukturOrganisasiPPID />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ListStrukturOrganisasiPPID() {
|
||||
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
|
||||
|
||||
useEffect(() => {
|
||||
stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
|
||||
if (!stateOrganisasi.findMany.data || stateOrganisasi.findMany.data.length === 0) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: Group pegawai berdasarkan posisiId
|
||||
const posisiMap = new Map<string, any>();
|
||||
|
||||
for (const pegawai of stateOrganisasi.findMany.data) {
|
||||
const posisiId = pegawai.posisi.id;
|
||||
if (!posisiMap.has(posisiId)) {
|
||||
posisiMap.set(posisiId, {
|
||||
...pegawai.posisi,
|
||||
pegawaiList: [],
|
||||
children: []
|
||||
});
|
||||
}
|
||||
posisiMap.get(posisiId)!.pegawaiList.push(pegawai);
|
||||
}
|
||||
|
||||
|
||||
// Step 2: Buat struktur pohon berdasarkan parentId
|
||||
let root: any[] = [];
|
||||
|
||||
posisiMap.forEach((posisi) => {
|
||||
if (posisi.parentId) {
|
||||
const parent = posisiMap.get(posisi.parentId);
|
||||
if (parent) {
|
||||
parent.children.push(posisi);
|
||||
}
|
||||
} else {
|
||||
root.push(posisi);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Ubah struktur ke format OrganizationChart
|
||||
function toOrgChartFormat(node: any): any {
|
||||
return {
|
||||
expanded: true,
|
||||
type: 'person',
|
||||
styleClass: 'p-person',
|
||||
data: {
|
||||
name: node.pegawaiList?.[0]?.namaLengkap || 'Tidak ada pegawai',
|
||||
status: node.nama,
|
||||
image: node.pegawaiList?.[0]?.image?.link || '/img/default.png'
|
||||
},
|
||||
children: node.children.map(toOrgChartFormat)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const chartData = root.map(toOrgChartFormat);
|
||||
|
||||
return (
|
||||
<Box py={10}>
|
||||
<Paper bg={colors.grey} p="md" style={{overflowX: 'auto'}}>
|
||||
<OrganizationChart value={chartData} nodeTemplate={nodeTemplate} />
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function nodeTemplate(node: any) {
|
||||
const imageSrc = node?.data?.image || '/img/default.png';
|
||||
const name = node?.data?.name || 'Tanpa Nama';
|
||||
const status = node?.data?.status || 'Tidak ada deskripsi';
|
||||
|
||||
return (
|
||||
<Stack align="center" gap={4}>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={name}
|
||||
radius="xl"
|
||||
w={120}
|
||||
h={120}
|
||||
fit="cover"
|
||||
/>
|
||||
<Text fw={600} ta="center">{name}</Text>
|
||||
<Text size="sm" c="dimmed" ta="center">{status}</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default StrukturOrganisasiPPID;
|
||||
Reference in New Issue
Block a user