Fix QC Kak Inno Admin, Fix QC Keano UI User, Fix QC Pak jun tabel apbdes

This commit is contained in:
2025-11-12 17:42:31 +08:00
parent 417a8937f5
commit 9622eb5a9a
354 changed files with 11444 additions and 4012 deletions

View File

@@ -66,7 +66,7 @@ export default function CreateDaftarInformasi() {
</Text>
<TextInput
placeholder="Contoh: Profil Desa, Laporan Keuangan, dll"
defaultValue={daftarInformasi.create.form.jenisInformasi}
value={daftarInformasi.create.form.jenisInformasi}
onChange={(e) => {
daftarInformasi.create.form.jenisInformasi = e.target.value;
}}
@@ -94,7 +94,7 @@ export default function CreateDaftarInformasi() {
</Text>
<TextInput
type="date"
defaultValue={daftarInformasi.create.form.tanggal}
value={daftarInformasi.create.form.tanggal}
onChange={(e) => {
daftarInformasi.create.form.tanggal = e.target.value;
}}

View File

@@ -1,6 +1,6 @@
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { Loader, Box, Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
@@ -8,6 +8,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateDasarHukumPPID from '../../../_state/ppid/dasar_hukum/dasarHukum';
import { toast } from 'react-toastify';
const PPIDTextEditor = dynamic(
() => import('../../_com/PPIDTextEditor').then(mod => mod.PPIDTextEditor),
@@ -20,6 +21,11 @@ function EditDasarHukum() {
// Gabungkan judul & content jadi satu state lokal
const [formData, setFormData] = useState({ judul: '', content: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
judul: '',
content: '',
});
// Load data awal sekali
useShallowEffect(() => {
@@ -35,24 +41,44 @@ function EditDasarHukum() {
judul: dasarHukumState.findById.data.judul ?? '',
content: dasarHukumState.findById.data.content ?? '',
});
setOriginalData({
judul: dasarHukumState.findById.data.judul ?? '',
content: dasarHukumState.findById.data.content ?? '',
});
}
}, [dasarHukumState.findById.data]);
const handleResetForm = () => {
setFormData({
judul: originalData.judul,
content: originalData.content,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = () => {
if (dasarHukumState.findById.data) {
// Update global state hanya saat submit
const updated = { ...dasarHukumState.findById.data, ...formData };
dasarHukumState.update.save(updated);
try {
setIsSubmitting(true);
if (dasarHukumState.findById.data) {
// Update global state hanya saat submit
const updated = { ...dasarHukumState.findById.data, ...formData };
dasarHukumState.update.save(updated);
}
router.push('/admin/ppid/dasar-hukum');
} catch (error) {
console.error("Error updating dasar hukum:", error);
toast.error("Terjadi kesalahan saat memperbarui dasar hukum");
} finally {
setIsSubmitting(false);
}
router.push('/admin/ppid/dasar-hukum');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Dasar Hukum PPID
</Title>
@@ -93,10 +119,22 @@ function EditDasarHukum() {
</Box>
</Box>
<Group justify="flex-end" mt="md">
{/* ======= Tombol Aksi ======= */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
loading={dasarHukumState.update.loading}
radius="md"
size="md"
style={{
@@ -105,7 +143,7 @@ function EditDasarHukum() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -64,7 +64,7 @@ function RespondenCreate() {
label="Nama"
type='text'
placeholder="masukkan nama"
defaultValue={stategrafikBerdasarkanResponden.create.form.name}
value={stategrafikBerdasarkanResponden.create.form.name}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.name = val.currentTarget.value;
}}
@@ -73,7 +73,7 @@ function RespondenCreate() {
label="Tanggal"
type="date"
placeholder="masukkan tanggal"
defaultValue={stategrafikBerdasarkanResponden.create.form.tanggal}
value={stategrafikBerdasarkanResponden.create.form.tanggal}
onChange={(val) => {
stategrafikBerdasarkanResponden.create.form.tanggal = val.currentTarget.value;
}}

View File

@@ -0,0 +1,300 @@
'use client';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import { ActionIcon, Alert, Box, Button, Center, Group, Image, Loader, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, 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';
import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID';
import Biodata from '../_com/biodata/biodataForm';
import PengalamanOrganisasi from '../_com/pengalaman_organisasi/pengalamanForm';
import ProgramKerjaUnggulan from '../_com/program_kerja_unggulan/programKerjaForm';
import RiwayatKarir from '../_com/riwayat_karir/riwayatKarirForm';
// import komponen rich text jika ada
function EditProfilePPID() {
const allState = useProxy(stateProfilePPID);
const router = useRouter();
const params = useParams();
const id = params?.id as string;
const [file, setFile] = useState<File | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!id) return;
stateProfilePPID.loadForEdit(id).then((data) => {
if (data?.image?.link) setPreviewImage(data.image.link);
});
}, [id]);
const handleFieldChange = (field: keyof typeof allState.editForm.form, value: string) => {
stateProfilePPID.editForm.updateField(field, value);
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
stateProfilePPID.editForm.loading = true;
let imageId = allState.editForm.form.imageId;
// Upload file baru jika ada
if (file) {
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
const uploaded = res.data?.data;
if (!uploaded?.id) {
toast.error("Gagal upload gambar");
return;
}
imageId = uploaded.id;
}
// Update form di state
allState.editForm.form.imageId = imageId;
const success = await stateProfilePPID.editForm.submit();
if (success) {
toast.success("Profil berhasil diperbarui!");
router.push("/admin/ppid/profil-ppid");
}
} catch (error) {
console.error("Error updating profile:", error);
toast.error("Terjadi kesalahan saat memperbarui profil");
} finally {
stateProfilePPID.editForm.loading = false;
setIsSubmitting(false);
}
};
const handleResetForm = () => {
if (!allState.profile.data) return;
// Reset form ke data awal yang di-load
const original = allState.profile.data;
stateProfilePPID.editForm.form = {
name: original.name || '',
imageId: original.imageId || '',
biodata: original.biodata || '',
riwayat: original.riwayat || '',
pengalaman: original.pengalaman || '',
unggulan: original.unggulan || '',
};
// Reset preview gambar juga
setPreviewImage(original.image?.link || null);
setFile(null);
toast.info('Perubahan dibatalkan');
};
const handleBack = () => router.back();
// Kondisi loading & error handling
if (allState.profile.loading) {
return (
<Center h={400}>
<Text>Memuat data profil...</Text>
</Center>
);
}
if (allState.profile.error) {
return (
<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>{allState.profile.error}</Text>
</Alert>
</Stack>
);
}
if (!allState.profile.data) {
return (
<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>Profil PPID tidak dapat ditemukan</Text>
</Alert>
</Stack>
);
}
return (
<Box p="md">
<Stack gap="md">
<Group mb="md">
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Profil PPID
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Title order={3} c={colors['blue-button']}>Edit Profil PPID</Title>
{/* Nama */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.editForm.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.editForm.form.name && "Nama wajib diisi"}
/>
{/* Upload Gambar */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2}
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>
Tarik gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text>
</div>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
{/* Form Rich Text */}
<Biodata
value={allState.editForm.form.biodata}
onChange={(val) => handleFieldChange('biodata', val)}
/>
<RiwayatKarir
value={allState.editForm.form.riwayat}
onChange={(val) => handleFieldChange('riwayat', val)}
/>
<PengalamanOrganisasi
value={allState.editForm.form.pengalaman}
onChange={(val) => handleFieldChange('pengalaman', val)}
/>
<ProgramKerjaUnggulan
value={allState.editForm.form.unggulan}
onChange={(val) => handleFieldChange('unggulan', val)}
/>
{/* ======= Tombol Aksi ======= */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditProfilePPID;

View File

@@ -40,7 +40,7 @@ function Page() {
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/ppid/profile-ppid/${allList.profile.data?.id}`)}
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
>
Edit
</Button>

View File

@@ -1,289 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Alert, Box, Button, Center, Group, Image, Paper, Stack, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../../_state/ppid/profile_ppid/profile_PPID';
import ApiFetch from '@/lib/api-fetch';
import { Dropzone } from '@mantine/dropzone';
import { IconAlertCircle, IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { toast } from 'react-toastify';
import Biodata from './biodata/biodataForm';
import PengalamanOrganisasi from './pengalaman_organisasi/pengalamanForm';
import ProgramKerjaUnggulan from './program_kerja_unggulan/programKerjaForm';
import RiwayatKarir from './riwayat_karir/riwayatKarirForm';
function EditProfilePPID() {
const allState = useProxy(stateProfilePPID);
const params = useParams();
const router = useRouter();
// UI States
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Load data on mount
useEffect(() => {
const loadData = async () => {
const id = params?.id as string;
if (!id) {
toast.error("ID tidak valid");
router.push("/admin/ppid/profile-ppid");
return;
}
try {
const profileData = await stateProfilePPID.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 () => {
stateProfilePPID.editForm.reset(); // cleanup form
};
}, [params?.id, router]);
const handleFieldChange = (field: string, value: string) => {
stateProfilePPID.editForm.updateField(field as any, value);
};
const handleSubmit = async () => {
if (isSubmitting || !stateProfilePPID.editForm.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;
}
stateProfilePPID.editForm.form.imageId = uploaded.id;
}
// Submit form
const success = await stateProfilePPID.editForm.submit();
if (success) {
toast.success("Berhasil menyimpan perubahan");
router.push("/admin/ppid/profile-ppid");
}
} catch (error) {
console.error("Error submitting form:", error);
toast.error("Gagal menyimpan profile");
} finally {
setIsSubmitting(false);
}
};
const handleBack = () => {
router.back();
};
// Loading state
if (allState.profile.loading) {
return (
<Box>
<Center h={400}>
<Text>Memuat data profile...</Text>
</Center>
</Box>
);
}
// Error state
if (allState.profile.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>{allState.profile.error}</Text>
</Alert>
</Stack>
</Box>
);
}
// No data state
if (!allState.profile.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 p="md">
<Stack gap="md">
<Group mb="md">
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Profil PPID
</Title>
</Group>
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors['white-1']}
p="md"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Title order={3} c={colors['blue-button']}>Edit Profile PPID</Title>
{/* Nama Field */}
<TextInput
label={<Text fw="bold">Nama Perbekel</Text>}
placeholder="Masukkan nama perbekel"
value={allState.editForm.form.name}
onChange={(e) => handleFieldChange('name', e.currentTarget.value)}
error={!allState.editForm.form.name && "Nama wajib diisi"}
/>
{/* File Upload */}
<Box>
<Text fz={"md"} fw={"bold"}>Gambar</Text>
<Box>
<Dropzone
onDrop={(files) => {
const selectedFile = files[0]; // Ambil file pertama
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile)); // Buat preview
}
}}
onReject={() => toast.error('File tidak valid.')}
maxSize={5 * 1024 ** 2} // Maks 5MB
accept={{ 'image/*': [] }}
>
<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>
Tarik gambar ke sini atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Maksimal 5MB dan harus format gambar
</Text>
</div>
</Group>
</Dropzone>
{/* Tampilkan preview kalau ada */}
{previewImage && (
<Box mt="sm">
<Image
src={previewImage}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '200px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid #ddd',
}}
loading='lazy'
/>
</Box>
)}
</Box>
</Box>
{/* Rich Components */}
<Biodata
value={allState.editForm.form.biodata}
onChange={(val) => handleFieldChange('biodata', val)}
/>
<RiwayatKarir
value={allState.editForm.form.riwayat}
onChange={(val) => handleFieldChange('riwayat', val)}
/>
<PengalamanOrganisasi
value={allState.editForm.form.pengalaman}
onChange={(val) => handleFieldChange('pengalaman', val)}
/>
<ProgramKerjaUnggulan
value={allState.editForm.form.unggulan}
onChange={(val) => handleFieldChange('unggulan', val)}
/>
{/* Submit Button */}
<Group>
<Button
bg={colors['blue-button']}
onClick={handleSubmit}
loading={isSubmitting || allState.editForm.loading}
disabled={!allState.editForm.form.name}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'}
</Button>
<Button
variant="outline"
onClick={handleBack}
disabled={isSubmitting || allState.editForm.loading}
>
Batal
</Button>
</Group>
</Stack>
</Paper>
</Stack>
</Box>
);
}
export default EditProfilePPID;

View File

@@ -4,6 +4,7 @@ import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
import {
ActionIcon,
Box,
Button,
Group,
@@ -13,7 +14,8 @@ import {
Stack,
Text,
TextInput,
Title
Title,
Loader
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
@@ -26,7 +28,20 @@ export default function EditPegawaiPPID() {
const router = useRouter();
const { id } = useParams<{ id: string }>();
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai);
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
namaLengkap: "",
gelarAkademik: "",
imageId: "",
tanggalMasuk: "",
email: "",
telepon: "",
alamat: "",
posisiId: "",
imageUrl: "",
isActive: true,
});
const [formData, setFormData] = useState({
namaLengkap: '',
gelarAkademik: '',
@@ -66,6 +81,18 @@ export default function EditPegawaiPPID() {
posisiId: data.posisiId || '',
isActive: data.isActive ?? true,
});
setOriginalData({
namaLengkap: data.namaLengkap || '',
gelarAkademik: data.gelarAkademik || '',
imageId: data.imageId || '',
tanggalMasuk: data.tanggalMasuk || '',
email: data.email || '',
telepon: data.telepon || '',
alamat: data.alamat || '',
posisiId: data.posisiId || '',
imageUrl: data.image?.link || '',
isActive: data.isActive ?? true,
});
setPreviewImage(data.image?.link || null);
}
@@ -78,8 +105,26 @@ export default function EditPegawaiPPID() {
loadPegawai();
}, [id]);
const handleResetForm = () => {
setFormData({
namaLengkap: originalData.namaLengkap,
gelarAkademik: originalData.gelarAkademik,
imageId: originalData.imageId,
tanggalMasuk: originalData.tanggalMasuk,
email: originalData.email,
telepon: originalData.telepon,
alamat: originalData.alamat,
posisiId: originalData.posisiId,
isActive: originalData.isActive,
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!formData.namaLengkap.trim()) {
return toast.error('Nama lengkap tidak boleh kosong');
}
@@ -102,15 +147,17 @@ export default function EditPegawaiPPID() {
} catch (error) {
console.error('Error updating pegawai:', error);
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui data pegawai');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">Edit Data Pegawai PPID</Title>
</Group>
@@ -163,20 +210,44 @@ export default function EditPegawaiPPID() {
<Dropzone.Idle><IconPhoto size={48} color="#868e96" stroke={1.5} /></Dropzone.Idle>
<Stack gap="xs" align="center">
<Text size="md" fw={500}>Seret gambar atau klik untuk memilih file</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar wajib</Text>
<Text size="sm" c="dimmed">Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp</Text>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="sm" style={{ display: 'flex', justifyContent: 'center' }}>
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{ maxHeight: 220, objectFit: 'contain', border: `1px solid ${colors['blue-button']}` }}
style={{
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading="lazy"
/>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -219,12 +290,29 @@ export default function EditPegawaiPPID() {
<Box>
<Text fw="bold" fz="sm" mb={6}>Posisi</Text>
<Select
label="Posisi"
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findManyAll.data?.map(p => ({ value: p.id, label: p.nama })) || []}
value={formData.posisiId}
onChange={(value) => value && setFormData({ ...formData, posisiId: value })}
data={stateStrukturPPID.posisiOrganisasi.findManyAll.data?.map((item) => ({
label: item.nama,
value: item.id,
})) || []}
value={formData.posisiId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateStrukturPPID.posisiOrganisasi.findManyAll.data?.find(
(item) => item.id === val
);
if (selected) {
formData.posisiId = selected.id;
}
} else {
formData.posisiId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
</Box>
@@ -243,10 +331,20 @@ export default function EditPegawaiPPID() {
</Box>
{/* Submit Button */}
<Group justify="flex-end" mt="md">
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
@@ -255,7 +353,7 @@ export default function EditPegawaiPPID() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -3,7 +3,7 @@
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 { ActionIcon, Box, Button, Group, Image, Paper, Select, Stack, Text, TextInput, Title, Loader } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -13,8 +13,12 @@ import { useProxy } from 'valtio/utils';
function CreatePegawaiPPID() {
const router = useRouter();
const [previewImage, setPreviewImage] = useState<{ preview: string; file: File } | null>(null);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null);
const stateOrganisasi = useProxy(stateStrukturPPID.pegawai)
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
stateStrukturPPID.posisiOrganisasi.findManyAll.load();
resetForm();
@@ -32,23 +36,23 @@ function CreatePegawaiPPID() {
posisiId: "",
isActive: true,
};
setPreviewImage(null);
setFile(null);
};
const handleSubmit = async () => {
if (!previewImage) {
return toast.warn("Pilih file gambar terlebih dahulu");
}
try {
// Upload gambar dulu
setIsSubmitting(true);
if (!file) {
return toast.warn('Silakan pilih file gambar terlebih dahulu');
}
const res = await ApiFetch.api.fileStorage.create.post({
file: previewImage.file,
name: previewImage.file.name,
file,
name: file.name,
});
const uploaded = res.data?.data;
if (!uploaded?.id) {
return toast.error("Gagal upload gambar");
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
}
// Set status aktif secara otomatis
@@ -68,15 +72,17 @@ function CreatePegawaiPPID() {
} catch (error) {
console.error("Error creating pegawai:", error);
toast.error("Terjadi kesalahan saat menambahkan pegawai");
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Pegawai PPID
</Title>
@@ -95,7 +101,7 @@ function CreatePegawaiPPID() {
<TextInput
label="Nama Lengkap"
placeholder="Masukkan nama lengkap"
defaultValue={stateOrganisasi.create.form.namaLengkap}
value={stateOrganisasi.create.form.namaLengkap}
onChange={(e) => (stateOrganisasi.create.form.namaLengkap = e.currentTarget.value)}
required
/>
@@ -104,7 +110,7 @@ function CreatePegawaiPPID() {
<TextInput
label="Gelar Akademik"
placeholder="Contoh: S.Kom"
defaultValue={stateOrganisasi.create.form.gelarAkademik}
value={stateOrganisasi.create.form.gelarAkademik}
onChange={(e) => (stateOrganisasi.create.form.gelarAkademik = e.currentTarget.value)}
/>
</Box>
@@ -114,71 +120,72 @@ function CreatePegawaiPPID() {
</Text>
<Dropzone
onDrop={(files) => {
const file = files[0];
if (file) {
setPreviewImage({
file,
preview: URL.createObjectURL(file)
});
const selectedFile = files[0];
if (selectedFile) {
setFile(selectedFile);
setPreviewImage(URL.createObjectURL(selectedFile));
}
}}
maxSize={5 * 1024 ** 2} // 5MB
accept={{
'image/*': ['.jpeg', '.jpg', '.png', '.webp']
}}
styles={{
root: {
border: '2px dashed #ced4da',
borderRadius: '8px',
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: '#228be6',
},
},
}}
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
maxSize={5 * 1024 ** 2}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
radius="md"
p="xl"
>
<Group justify="center" gap="xl" mih={160} style={{ pointerEvents: 'none' }}>
<Group justify="center" gap="xl" mih={180}>
<Dropzone.Accept>
<IconUpload size={52} color="var(--mantine-color-blue-6)" stroke={1.5} />
<IconUpload size={48} color={colors['blue-button']} stroke={1.5} />
</Dropzone.Accept>
<Dropzone.Reject>
<IconX size={52} color="var(--mantine-color-red-6)" stroke={1.5} />
<IconX size={48} color="red" stroke={1.5} />
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto size={52} color="var(--mantine-color-dimmed)" stroke={1.5} />
<IconPhoto size={48} color="#868e96" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="md" inline>
Seret gambar ke sini atau klik untuk memilih file
<Stack gap="xs" align="center">
<Text size="md" fw={500}>
Seret gambar atau klik untuk memilih file
</Text>
<Text size="sm" c="dimmed" inline mt={7}>
Format yang didukung: JPG, PNG, WebP. Maksimal 5MB
<Text size="sm" c="dimmed">
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
</Text>
</div>
</Stack>
</Group>
</Dropzone>
{previewImage && (
<Box mt="md">
<Text fw="bold" fz="sm" mb={6}>
Preview Gambar
</Text>
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
<Image
src={previewImage.preview}
alt="Preview"
width={200}
height={200}
fit="cover"
src={previewImage}
alt="Preview Gambar"
radius="md"
style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
maxHeight: 200,
objectFit: 'contain',
border: '1px solid #ddd',
}}
loading='lazy'
loading="lazy"
/>
{/* Tombol hapus (pojok kanan atas) */}
<ActionIcon
variant="filled"
color="red"
radius="xl"
size="sm"
pos="absolute"
top={5}
right={5}
onClick={() => {
setPreviewImage(null);
setFile(null);
}}
style={{
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
}}
>
<IconX size={14} />
</ActionIcon>
</Box>
)}
</Box>
@@ -187,7 +194,7 @@ function CreatePegawaiPPID() {
label="Tanggal Masuk"
type="date"
placeholder="Contoh: 2022-01-01"
defaultValue={stateOrganisasi.create.form.tanggalMasuk}
value={stateOrganisasi.create.form.tanggalMasuk}
onChange={(e) => (stateOrganisasi.create.form.tanggalMasuk = e.currentTarget.value)}
/>
</Box>
@@ -197,16 +204,17 @@ function CreatePegawaiPPID() {
label="Email"
type="email"
placeholder="Contoh: email@example.com"
defaultValue={stateOrganisasi.create.form.email}
value={stateOrganisasi.create.form.email}
onChange={(e) => (stateOrganisasi.create.form.email = e.currentTarget.value)}
/>
</Box>
<Box>
<TextInput
type='number'
label="Nomor Telepon"
placeholder="Contoh: 08123456789"
defaultValue={stateOrganisasi.create.form.telepon}
value={stateOrganisasi.create.form.telepon}
onChange={(e) => (stateOrganisasi.create.form.telepon = e.currentTarget.value)}
/>
</Box>
@@ -215,7 +223,7 @@ function CreatePegawaiPPID() {
<TextInput
label="Alamat"
placeholder="Contoh: Jl. Contoh No. 1"
defaultValue={stateOrganisasi.create.form.alamat}
value={stateOrganisasi.create.form.alamat}
onChange={(e) => (stateOrganisasi.create.form.alamat = e.currentTarget.value)}
/>
</Box>
@@ -225,6 +233,31 @@ function CreatePegawaiPPID() {
Posisi
</Text>
<Select
label="Kategori"
placeholder="Pilih kategori"
data={stateStrukturPPID.posisiOrganisasi.findManyAll.data?.map((item) => ({
label: item.nama,
value: item.id,
})) || []}
value={stateOrganisasi.create.form.posisiId || null}
onChange={(val: string | null) => {
if (val) {
const selected = stateStrukturPPID.posisiOrganisasi.findManyAll.data?.find(
(item) => item.id === val
);
if (selected) {
stateOrganisasi.create.form.posisiId = selected.id;
}
} else {
stateOrganisasi.create.form.posisiId = '';
}
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
required
/>
{/* <Select
placeholder="Pilih posisi"
data={stateStrukturPPID.posisiOrganisasi.findManyAll.data?.map(p => ({
value: p.id,
@@ -236,11 +269,24 @@ function CreatePegawaiPPID() {
}}
searchable
clearable
/>
/> */}
</Box>
<Group justify="flex-end" mt="md">
{/* ======= Tombol Aksi ======= */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
@@ -251,7 +297,7 @@ function CreatePegawaiPPID() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -5,7 +5,7 @@
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 { Loader, 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';
@@ -24,6 +24,14 @@ function EditPosisiOrganisasiPPID() {
hierarki: 0,
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
nama: "",
deskripsi: "",
hierarki: 0,
});
// Fungsi generik untuk update formData
const handleChange = (field: keyof typeof formData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -42,6 +50,11 @@ function EditPosisiOrganisasiPPID() {
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
});
setOriginalData({
nama: data.nama || '',
deskripsi: data.deskripsi || '',
hierarki: data.hierarki || 0,
});
}
} catch (err) {
console.error('Error loading posisi organisasi:', err);
@@ -52,6 +65,15 @@ function EditPosisiOrganisasiPPID() {
loadPosisiOrganisasi();
}, [id]);
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
deskripsi: originalData.deskripsi,
hierarki: originalData.hierarki,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
if (!formData.nama.trim()) {
toast.error('Nama posisi organisasi tidak boleh kosong');
@@ -59,6 +81,7 @@ function EditPosisiOrganisasiPPID() {
}
try {
setIsSubmitting(true);
// Update global state hanya saat submit
stateOrganisasi.edit.form = {
nama: formData.nama.trim(),
@@ -78,15 +101,17 @@ function EditPosisiOrganisasiPPID() {
} catch (err) {
console.error('Error updating posisi organisasi:', err);
// toast error biasanya sudah ada di update
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Posisi Organisasi PPID
</Title>
@@ -132,10 +157,21 @@ function EditPosisiOrganisasiPPID() {
required
/>
<Group justify="flex-end" mt="md">
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
loading={stateOrganisasi.edit.loading}
radius="md"
size="md"
style={{
@@ -144,7 +180,7 @@ function EditPosisiOrganisasiPPID() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -3,57 +3,54 @@
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, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Loader, Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreatePosisiOrganisasiPPID() {
const router = useRouter();
const stateOrganisasi = useProxy(stateStrukturPPID.posisiOrganisasi);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
stateOrganisasi.findMany.load();
// Initialize form with default values
}, []);
const resetForm = () => {
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
nama: '',
deskripsi: '',
hierarki: 0,
};
return () => {
// Clean up form on unmount
stateOrganisasi.create.form = {
nama: "",
deskripsi: "",
hierarki: 0,
};
};
}, []);
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
if (!stateOrganisasi.create.form.nama.trim()) {
return toast.error('Nama posisi tidak boleh kosong');
}
await stateOrganisasi.create.submit();
toast.success('Posisi organisasi berhasil ditambahkan');
router.push('/admin/ppid/struktur-ppid/posisi-organisasi');
} catch (error) {
toast.error('Gagal menambahkan posisi organisasi');
console.error('Error:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Posisi Organisasi PPID
</Title>
@@ -71,11 +68,11 @@ function CreatePosisiOrganisasiPPID() {
<TextInput
label="Nama Posisi"
placeholder="Contoh: Kepala Desa"
defaultValue={stateOrganisasi.create.form.nama}
value={stateOrganisasi.create.form.nama}
onChange={(e) => (stateOrganisasi.create.form.nama = e.target.value)}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi
@@ -87,13 +84,13 @@ function CreatePosisiOrganisasiPPID() {
}}
/>
</Box>
<TextInput
label="Hierarki"
type="number"
min={0}
placeholder="Contoh: 1 (Angka semakin kecil, posisi semakin tinggi)"
defaultValue={stateOrganisasi.create.form.hierarki || ''}
value={stateOrganisasi.create.form.hierarki || ''}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
stateOrganisasi.create.form.hierarki = isNaN(value) ? 0 : value;
@@ -101,10 +98,21 @@ function CreatePosisiOrganisasiPPID() {
required
/>
<Group justify="flex-end" mt="md">
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
loading={stateOrganisasi.create.loading}
radius="md"
size="md"
style={{
@@ -113,7 +121,7 @@ function CreatePosisiOrganisasiPPID() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>

View File

@@ -1,6 +1,6 @@
'use client';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Title } from '@mantine/core';
import { Loader, Box, Button, Group, Paper, Stack, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
@@ -9,6 +9,7 @@ import { useProxy } from 'valtio/utils';
import stateVisiMisiPPID from '../../../_state/ppid/visi_misi_ppid/visimisiPPID';
import MisiPPID from '../misiPPID/misi-PPID';
import VisiPPID from '../visiPPID/visi-PPID';
import { toast } from 'react-toastify';
function VisiMisiPPIDEdit() {
const router = useRouter();
@@ -16,6 +17,8 @@ function VisiMisiPPIDEdit() {
// Gabung state lokal menjadi satu object
const [formData, setFormData] = useState({ visi: '', misi: '' });
const [originalData, setOriginalData] = useState({ visi: '', misi: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
// Initialize global data ke state lokal saat pertama load
useShallowEffect(() => {
@@ -30,6 +33,10 @@ function VisiMisiPPIDEdit() {
visi: visiMisi.findById.data.visi ?? '',
misi: visiMisi.findById.data.misi ?? '',
});
setOriginalData({
visi: visiMisi.findById.data.visi ?? '',
misi: visiMisi.findById.data.misi ?? '',
});
}
}, [visiMisi.findById.data]);
@@ -37,22 +44,38 @@ function VisiMisiPPIDEdit() {
setFormData(prev => ({ ...prev, [key]: value }));
};
const handleResetForm = () => {
setFormData({
visi: originalData.visi,
misi: originalData.misi,
});
toast.info('Form dikembalikan ke data awal');
};
const submit = () => {
if (visiMisi.findById.data) {
// update nilai global hanya saat submit
visiMisi.findById.data.visi = formData.visi;
visiMisi.findById.data.misi = formData.misi;
visiMisi.update.save(visiMisi.findById.data);
try {
setIsSubmitting(true);
if (visiMisi.findById.data) {
// update nilai global hanya saat submit
visiMisi.findById.data.visi = formData.visi;
visiMisi.findById.data.misi = formData.misi;
visiMisi.update.save(visiMisi.findById.data);
}
router.push('/admin/ppid/visi-misi-ppid');
} catch (error) {
console.error("Error updating visi misi:", error);
toast.error("Terjadi kesalahan saat memperbarui visi misi");
} finally {
setIsSubmitting(false);
}
router.push('/admin/ppid/visi-misi-ppid');
};
return (
<Box px={{ base: 'sm', md: 'lg' }} py="md">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Visi Misi PPID
</Title>
@@ -75,10 +98,22 @@ function VisiMisiPPIDEdit() {
<MisiPPID value={formData.misi} onChange={value => handleChange('misi', value)} />
</Box>
<Group justify="flex-end" mt="md">
{/* ======= Tombol Aksi ======= */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={submit}
loading={visiMisi.update.loading}
radius="md"
size="md"
style={{
@@ -87,7 +122,7 @@ function VisiMisiPPIDEdit() {
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>