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

@@ -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>