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

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

@@ -0,0 +1,28 @@
'use client'
import { Box, Text } from '@mantine/core';
import EditPPIDEditor from '../../_com/editPPIDEditor';
function Biodata({
value,
onChange,
error,
}: {
value: string;
onChange: (val: string) => void;
error?: string;
}) {
return (<Box>
<Text fw={"bold"}>Biodata</Text>
<EditPPIDEditor
value={value}
onChange={onChange}
/>
{error && <Text c="red" size="sm">{error}</Text>}
</Box>
);
}
export default Biodata;

View File

@@ -0,0 +1,101 @@
'use client'
import { RichTextEditor, Link } from '@mantine/tiptap';
import { useEditor } from '@tiptap/react';
import Highlight from '@tiptap/extension-highlight';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import Superscript from '@tiptap/extension-superscript';
import SubScript from '@tiptap/extension-subscript';
import { useEffect } from 'react';
type EditEditorProps = {
value: string;
onChange: (content: string) => void;
};
export default function EditPPIDEditor({ value, onChange }: EditEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Underline,
Link,
Superscript,
SubScript,
Highlight,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
],
onUpdate({ editor }) {
onChange(editor.getHTML());
},
});
useEffect(() => {
if (editor && value && value !== editor.getHTML()) {
editor.commands.setContent(value);
}
}, [editor, value]);
useEffect(() => {
if (!editor) return;
const updateHandler = () => onChange(editor.getHTML());
editor.on('update', updateHandler);
return () => {
editor.off('update', updateHandler);
};
}, [editor, onChange]);
return (
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar sticky stickyOffset="var(--docs-header-height)">
{/* Toolbar seperti sebelumnya */}
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
<RichTextEditor.Underline />
<RichTextEditor.Strikethrough />
<RichTextEditor.ClearFormatting />
<RichTextEditor.Highlight />
<RichTextEditor.Code />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.H1 />
<RichTextEditor.H2 />
<RichTextEditor.H3 />
<RichTextEditor.H4 />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Blockquote />
<RichTextEditor.Hr />
<RichTextEditor.BulletList />
<RichTextEditor.OrderedList />
<RichTextEditor.Subscript />
<RichTextEditor.Superscript />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Link />
<RichTextEditor.Unlink />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.AlignLeft />
<RichTextEditor.AlignCenter />
<RichTextEditor.AlignJustify />
<RichTextEditor.AlignRight />
</RichTextEditor.ControlsGroup>
<RichTextEditor.ControlsGroup>
<RichTextEditor.Undo />
<RichTextEditor.Redo />
</RichTextEditor.ControlsGroup>
</RichTextEditor.Toolbar>
<RichTextEditor.Content />
</RichTextEditor>
);
}

View File

@@ -0,0 +1,26 @@
'use client'
import { Box, Text } from '@mantine/core';
import EditPPIDEditor from '../../_com/editPPIDEditor';
function PengalamanOrganisasi({
value,
onChange,
error,
}: {
value: string;
onChange: (val: string) => void;
error?: string;
}) {
return (<Box>
<Text fw={"bold"}>Pengalaman Organisasi</Text>
<EditPPIDEditor
value={value}
onChange={onChange}
/>
{error && <Text c="red" size="sm">{error}</Text>}
</Box>
);
}
export default PengalamanOrganisasi;

View File

@@ -0,0 +1,26 @@
'use client'
import { Box, Text } from '@mantine/core';
import EditPPIDEditor from '../../_com/editPPIDEditor';
function ProgramKerjaUnggulan({
value,
onChange,
error,
}: {
value: string;
onChange: (val: string) => void;
error?: string;
}) {
return (<Box>
<Text fw={"bold"}>Program Kerja Unggulan</Text>
<EditPPIDEditor
value={value}
onChange={onChange}
/>
{error && <Text c="red" size="sm">{error}</Text>}
</Box>
);
}
export default ProgramKerjaUnggulan;

View File

@@ -0,0 +1,29 @@
'use client';
import { Box, Text } from '@mantine/core';
import EditPPIDEditor from '../../_com/editPPIDEditor';
function RiwayatKarir({
value,
onChange,
error,
}: {
value: string;
onChange: (val: string) => void;
error?: string;
}) {
return (
<Box>
<Text fw={"bold"}>Riwayat Karir</Text>
<EditPPIDEditor
value={value}
onChange={onChange}
/>
{error && <Text c="red" size="sm">{error}</Text>}
</Box>
);
}
export default RiwayatKarir;

View File

@@ -0,0 +1,129 @@
'use client'
import colors from '@/con/colors';
import { Box, Button, Center, Divider, Grid, GridCol, Image, Paper, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconEdit } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import stateProfilePPID from '../../_state/ppid/profile_ppid/profile_PPID';
function Page() {
const router = useRouter()
const allList = useProxy(stateProfilePPID)
useShallowEffect(() => {
allList.profile.load("edit") // Assuming "1" is your default ID, adjust as needed
}, [])
if (!allList.profile.data) {
return (
<Stack align="center" justify="center" py="xl">
<Skeleton radius="md" height={800} />
</Stack>
);
}
const dataArray = Array.isArray(allList.profile.data)
? allList.profile.data
: [allList.profile.data];
return (
<Paper bg={colors['white-1']} p="lg" radius="md" shadow="sm">
<Stack gap="md">
<Grid align="center">
<GridCol span={{ base: 12, md: 11 }}>
<Title order={3} c={colors['blue-button']}>Preview Profil PPID</Title>
</GridCol>
<GridCol span={{ base: 12, md: 1 }}>
<Button
c="green"
variant="light"
leftSection={<IconEdit size={18} stroke={2} />}
radius="md"
onClick={() => router.push(`/admin/ppid/profil-ppid/${allList.profile.data?.id}`)}
>
Edit
</Button>
</GridCol>
</Grid>
{dataArray.map((item) => (
<Paper key={item.id} p="xl" bg={'white'} withBorder radius="md" shadow="xs">
<Box px={{ base: "sm", md: 100 }}>
<Grid>
<GridCol span={12}>
<Center>
<Image loading='lazy' src="/darmasaba-icon.png" w={{ base: 100, md: 150 }} alt="Logo Desa" />
</Center>
</GridCol>
<GridCol span={12}>
<Text ta="center" fz={{ base: "1.2rem", md: "1.8rem" }} fw="bold" c={colors['blue-button']}>
PROFIL PIMPINAN BADAN PUBLIK DESA DARMASABA
</Text>
</GridCol>
</Grid>
</Box>
<Divider my="md" color={colors['blue-button']} />
<Box px={{ base: 0, md: 50 }} pb="xl">
<Paper bg={colors['BG-trans']} radius="md" shadow="xs" p="lg">
<Stack gap={0}>
<Center>
<Image
pt={{ base: 0, md: 60 }}
src={item.image?.link || "/perbekel.png"}
w="100%"
maw={300}
alt="Foto Profil PPID"
radius="md"
onError={(e) => { e.currentTarget.src = "/perbekel.png"; }}
loading='lazy'
/>
</Center>
<Paper
bg={colors['blue-button']}
py="md"
px="sm"
radius="md"
className="glass3"
style={{ mt: -30, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<Text ta="center" c={colors['white-1']} fw="bolder" fz={{ base: "1.2rem", md: "1.6rem" }}>
{item.name}
</Text>
</Paper>
</Stack>
</Paper>
<Box mt="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Biodata</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.biodata }} />
</Box>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Riwayat Karir</Text>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.riwayat }} />
</Box>
<Box mt="xl">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Pengalaman Organisasi</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.pengalaman }} />
</Box>
</Box>
<Box mt="xl" mb="lg">
<Text fz={{ base: "1.125rem", md: "1.5rem" }} fw="bold" mb={4}>Program Kerja Unggulan</Text>
<Box px={20}>
<Text fz={{ base: "1rem", md: "1.4rem" }} ta="justify" c={colors['blue-button']} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: item.unggulan }} />
</Box>
</Box>
</Box>
</Paper>
))}
</Stack>
</Paper>
)
}
export default Page;