import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from '@/components/ui/command'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty'; import { Input } from '@/components/ui/input'; import { InputGroup, InputGroupAddon, InputGroupInput, } from '@/components/ui/input-group'; import { Label } from '@/components/ui/label'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { useAuthBreadcrumbOperator } from '@/hooks/use-auth-breadcrumb-operator'; import AppLayout from '@/layouts/app-layout'; import { Head, router, useForm } from '@inertiajs/react'; import { DialogDescription } from '@radix-ui/react-dialog'; import { CommandList } from 'cmdk'; import jsPDF from 'jspdf'; import 'jspdf-autotable'; import autoTable from 'jspdf-autotable'; import { Check, ChevronsUpDown, Edit, Eye, FolderCodeIcon, PlusCircle, Search, Trash2, } from 'lucide-react'; import { useEffect, useState } from 'react'; import { toast, Toaster } from 'sonner'; const cn = (...classes: (string | boolean | undefined | null)[]) => classes.filter(Boolean).join(' '); interface SakipNode { id: number; uraian: string; kategori: string; satuan: string; target: string; parent_id: string | number; children?: { uraian: string; satuan?: string; target?: string }[]; } interface Program { id: number; nama: string; tahun_id: string | number; kegiatans?: Kegiatan[]; } interface Kegiatan { id: number; nama: string; program_id: number; subkegiatans?: Subkegiatan[]; } interface Subkegiatan { id: number; nama: string; kegiatan_id: number; anggaran?: string; } type PkNode = { id: number; asn_id: number; tahun_id: number; tingkat: string; uraian?: string | null; asn?: { nama: string; nip?: string; jabatan: string }; sakipNode?: { id?: number; uraian?: string; kategori?: string; parent_id?: number; }; program?: { id?: number; nama?: string }; kegiatan?: { id?: number; nama?: string }; subkegiatan?: { id?: number; nama?: string; anggaran?: string }; // Multi fields (backward-compatible) sakipNodes?: SakipNode[]; programs?: Program[]; kegiatans?: { id: number; nama: string }[]; subkegiatans?: Subkegiatan[]; indikator?: { uraian: string }; asn_kedua_id?: number | string | null; nama_kedua?: string | null; nip_kedua?: string | null; jabatan_kedua?: string | null; }; interface Asn { id: number; nama: string; nip: string; jabatan: string; } interface Tahun { id: number; nama: string; jenis?: string; } interface Props { pkList?: PkNode[]; asnList?: Asn[]; tahunList?: Tahun[]; sakipNodeList?: SakipNode[]; programList?: Program[]; kegiatanList?: Kegiatan[]; subkegList?: Subkegiatan[]; } export default function PkIndex({ pkList = [], asnList = [], tahunList = [], sakipNodeList = [], programList = [], kegiatanList = [], subkegList = [], }: Props) { const breadcrumbs = useAuthBreadcrumbOperator(); const [pdfUrl, setPdfUrl] = useState(null); const [showPdfModal, setShowPdfModal] = useState(false); const [showFormModal, setShowFormModal] = useState(false); const [isEdit, setIsEdit] = useState(false); const [currentId, setCurrentId] = useState(null); // pencarian const [searchQuery, setSearchQuery] = useState(''); // Multi-select states (store ids as strings) const [selectedSakipNodes, setSelectedSakipNodes] = useState([]); const [selectedPrograms, setSelectedPrograms] = useState([]); const [selectedKegiatans, setSelectedKegiatans] = useState([]); const [selectedSubKegiatans, setSelectedSubKegiatans] = useState( [], ); // hapus const [showDeleteAlert, setShowDeleteAlert] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); // State Popover disatukan untuk penyederhanaan const [openPopover, setOpenPopover] = useState({ asn: false, sakip: false, program: false, kegiatan: false, subkegiatan: false, pihakKedua: false, }); const form = useForm({ asn_id: '', tahun_id: '', tingkat: '', nama_kedua: '', nip_kedua: '', jabatan_kedua: '', sakip_node_ids: [] as string[], program_ids: [] as string[], kegiatan_ids: [] as string[], subkegiatan_ids: [] as string[], uraian: '', asn_kedua_id: '', }); const getSakipKategoriByTingkat = (tingkat: string) => { switch (tingkat) { case 'eselon_2': return 'sasaran pd'; case 'eselon_3': return [ 'sasaran program', 'indikator sasaran program', 'sasaran kegiatan', 'indikator sasaran kegiatan', ]; case 'eselon_4': return 'sasaran sub kegiatan'; case 'staf': return 'sasaran sub kegiatan staf'; case 'fungsional': return 'sasaran sub kegiatan staf'; default: return null; } }; const resetFormState = () => { form.reset(); setIsEdit(false); setCurrentId(null); setSelectedSakipNodes([]); setSelectedPrograms([]); setSelectedKegiatans([]); setSelectedSubKegiatans([]); }; const openAddModal = () => { resetFormState(); setShowFormModal(true); }; // Helper untuk menangani backward-compatible multi/single fields const getIdsFromItem = ( multiField: { id: number }[] | undefined, singleField: { id?: number } | undefined, ): string[] => { if (multiField) return multiField.map((p) => p.id.toString()); if (singleField?.id) return [singleField.id.toString()]; return []; }; const openEditModal = (item: PkNode) => { setIsEdit(true); setCurrentId(item.id); const sakipFromItem = getIdsFromItem(item.sakipNodes, item.sakipNode); const programsFromItem = getIdsFromItem(item.programs, item.program); const kegFromItem = getIdsFromItem( item.kegiatans as any, item.kegiatan, ); const subFromItem = getIdsFromItem( item.subkegiatans as any, item.subkegiatan, ); form.setData({ asn_id: item.asn_id?.toString() || '', tahun_id: item.tahun_id?.toString() || '', tingkat: item.tingkat || 'staf', sakip_node_ids: sakipFromItem, program_ids: programsFromItem, kegiatan_ids: kegFromItem, subkegiatan_ids: subFromItem, uraian: item.uraian || '', // ✅ Tambahkan field pihak kedua nama_kedua: item.nama_kedua || '', nip_kedua: item.nip_kedua || '', jabatan_kedua: item.jabatan_kedua || '', asn_kedua_id: item.asn_kedua_id?.toString() || '', }); // Pastikan state lokal terisi setSelectedSakipNodes(sakipFromItem); setSelectedPrograms(programsFromItem); setSelectedKegiatans(kegFromItem); setSelectedSubKegiatans(subFromItem); setShowFormModal(true); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // form.data sudah terupdate secara real-time di toggleMultiSelect, // sehingga form.setData di sini dihapus untuk menghindari duplikasi. if (isEdit && currentId) { router.put(route('pk.update', currentId), form.data, { onSuccess: () => { form.reset(); setShowFormModal(false); resetFormState(); }, }); } else { router.post(route('pk.store'), form.data, { onSuccess: () => { form.reset(); setShowFormModal(false); resetFormState(); }, }); } }; function handleDelete(id: number) { router.delete(route('pk.destroy', id), { onStart: () => { toast.loading('Menghapus data...'); }, onSuccess: () => { toast.success('Data berhasil dihapus!', { style: { background: '#16a34a', // hijau color: 'white', }, }); }, onError: () => { toast.error('Gagal menghapus data!', { style: { background: '#dc2626', // merah color: 'white', }, }); }, onFinish: () => { toast.dismiss(); // menutup toast loading kalau ada }, }); } const toggleMultiSelect = ( list: string[], setList: (v: string[]) => void, id: string, key: | 'sakip_node_ids' | 'program_ids' | 'kegiatan_ids' | 'subkegiatan_ids', ) => { const isSelected = list.includes(id); const updated = isSelected ? list.filter((x) => x !== id) : [...list, id]; setList(updated); form.setData(key, updated); // Update form data immediately // Logika Casacading Deselect (Penyederhanaan & Perbaikan) if (isSelected) { if (key === 'program_ids') { // Deselecting Program -> reset Kegiatan & Subkegiatan setSelectedKegiatans([]); form.setData('kegiatan_ids', []); setSelectedSubKegiatans([]); form.setData('subkegiatan_ids', []); } else if (key === 'kegiatan_ids') { // Deselecting Kegiatan -> reset Subkegiatan setSelectedSubKegiatans([]); form.setData('subkegiatan_ids', []); } } }; const getBase64ImageFromUrl = async (url: string) => { const res = await fetch(url); const blob = await res.blob(); return new Promise((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); }; const previewPDF = async (item: PkNode) => { const doc = new jsPDF(); const logo = await getBase64ImageFromUrl('/logo.png'); const pageWidth = doc.internal.pageSize.getWidth(); const logoWidth = 16; const logoHeight = 20; const logoX = (pageWidth - logoWidth) / 2; const logoY = 10; doc.addImage(logo, 'PNG', logoX, logoY, logoWidth, logoHeight); // ====== HALAMAN 1 ====== doc.setFontSize(11); let textY = logoY + logoHeight + 8; doc.text('PERJANJIAN KINERJA TAHUN 2025', pageWidth / 2, textY, { align: 'center', }); textY += 6; doc.text( 'BADAN KEPEGAWAIAN DAN PENGEMBANGAN SUMBER DAYA MANUSIA', pageWidth / 2, textY, { align: 'center', }, ); textY += 6; doc.setFontSize(11); doc.text('KABUPATEN BANGGAI', pageWidth / 2, textY, { align: 'center', }); doc.setFontSize(11); // === Paragraf pembuka autoTable(doc, { startY: 60, theme: 'plain', body: [ [ { content: 'Dalam rangka mewujudkan manajemen pemerintahan yang efektif, transparan dan akuntable serta berorientasi pada hasil, kami yang bertanda tangan dibawah ini :', styles: { halign: 'justify' }, }, ], ], styles: { fontSize: 11, cellPadding: 1.5 }, margin: { left: 16 }, tableWidth: 'auto', }); let afterParagraphY = doc.lastAutoTable?.finalY ?? 50; // === Tabel utama (ASN) autoTable(doc, { startY: afterParagraphY, body: [ [{ content: 'Nama' }, ':', item.asn?.nama || '-'], [{ content: 'Jabatan' }, ':', item.asn?.jabatan || '-'], ], theme: 'plain', styles: { fontSize: 11, cellPadding: { top: 1, right: 3, bottom: 1, left: 3 }, }, columnStyles: { 0: { cellWidth: 40 }, 1: { cellWidth: 10, halign: 'center' }, 2: { cellWidth: 'auto' }, }, }); let afterMainTableY = doc.lastAutoTable?.finalY ?? afterParagraphY + 10; // === Pihak Pertama autoTable(doc, { startY: afterMainTableY + 5, theme: 'plain', body: [ [ { content: 'Selanjutnya disebut Pihak Pertama', styles: { halign: 'justify' }, }, ], ], styles: { fontSize: 11, cellPadding: 1.5 }, margin: { left: 16 }, tableWidth: 'auto', }); let afterExtraTableY = doc.lastAutoTable?.finalY ?? afterMainTableY + 10; // === Biodata Pihak Kedua if (item.tingkat === 'eselon_2') { autoTable(doc, { startY: afterExtraTableY + 5, body: [ ['Nama', ':', 'Ir. H. Amirudin, MM'], ['Jabatan', ':', 'Bupati Banggai'], ], theme: 'plain', styles: { fontSize: 11, cellPadding: { top: 1, right: 3, bottom: 1, left: 3 }, }, columnStyles: { 0: { cellWidth: 40 }, 1: { cellWidth: 10, halign: 'center' }, 2: { cellWidth: 'auto' }, }, }); } else { autoTable(doc, { startY: afterExtraTableY + 5, body: [ [{ content: 'Nama' }, ':', item.nama_kedua || '-'], [{ content: 'Jabatan' }, ':', item.jabatan_kedua || '-'], ], theme: 'plain', styles: { fontSize: 11, cellPadding: { top: 1, right: 3, bottom: 1, left: 3 }, }, columnStyles: { 0: { cellWidth: 40 }, 1: { cellWidth: 10, halign: 'center' }, 2: { cellWidth: 'auto' }, }, }); } let afterBiodataTableY = doc.lastAutoTable?.finalY ?? afterExtraTableY + 10; // === Pihak Kedua autoTable(doc, { startY: afterBiodataTableY + 2, theme: 'plain', body: [ [ { content: 'Selaku atasan Pihak Pertama, selanjutnya disebut Pihak Kedua.', styles: { halign: 'justify' }, }, ], ], styles: { fontSize: 11, cellPadding: 1.5 }, margin: { left: 16 }, tableWidth: 'auto', }); let afterPihakKeduaY = doc.lastAutoTable?.finalY ?? afterBiodataTableY + 10; // === Paragraf 2 autoTable(doc, { startY: afterPihakKeduaY + 5, theme: 'plain', body: [ [ { content: 'Pihak Pertama berjanji akan mewujudkan target kinerja yang seharusnya sesuai lampiran perjanjian ini, dalam rangka mencapai target kinerja jangka menengah seperti yang telah ditetapkan dalam dokumen perencanaan. Keberhasilan dan kegagalan pencapaian target kinerja tersebut menjadi tanggung jawab kami.', styles: { halign: 'justify' }, }, ], ], styles: { fontSize: 11, cellPadding: 1 }, margin: { left: 16 }, tableWidth: 'auto', }); let afterText3Y = doc.lastAutoTable?.finalY ?? afterPihakKeduaY + 6; // === Paragraf 3 autoTable(doc, { startY: afterText3Y + 5, theme: 'plain', body: [ [ { content: 'Pihak Kedua akan melakukan supervisi yang diperlukan serta akan melakukan evaluasi terhadap capaian kinerja dari perjanjian ini dan mengambil tindakan yang diperlukan dalam rangka pemberian penghargaan dan sanksi.', styles: { halign: 'justify' }, }, ], ], styles: { fontSize: 11, cellPadding: 1 }, margin: { left: 16 }, tableWidth: 'auto', }); let afterText4Y = doc.lastAutoTable?.finalY ?? afterText3Y + 6; // === Tanda tangan Halaman 1 if (item.tingkat === 'eselon_2') { autoTable(doc, { startY: afterText4Y + 10, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ 'BUPATI BANGGAI', '', { content: 'Kepala BKPSDM Kabupaten Banggai', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: 'Ir. H. Amirudin, MM', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ '', '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { // underline hanya untuk row ke-4 (nama pejabat) if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } else { autoTable(doc, { startY: afterText4Y + 10, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ { content: item.jabatan_kedua || '-', styles: { halign: 'center' }, }, '', { content: item.asn?.jabatan || '-', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: item.nama_kedua || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ { content: item.nip_kedua ? `NIP. ${item.nip_kedua}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { // underline hanya untuk row ke-4 (nama pejabat) if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } // ====== HALAMAN 2 ====== doc.addPage(); doc.setFontSize(12); doc.text('PERJANJIAN KINERJA TAHUN 2025', 105, 20, { align: 'center' }); doc.text( 'BADAN KEPEGAWAIAN DAN PENGEMBANGAN SUMBER DAYA MANUSIA', 105, 28, { align: 'center' }, ); doc.text('KABUPATEN BANGGAI', 105, 36, { align: 'center' }); if (item.tingkat === 'eselon_2') { const sasaranRows = (item.sakipNodes || []) .filter( (sn: SakipNode) => sn.kategori === getSakipKategoriByTingkat(item.tingkat), ) .map((sasaran: SakipNode, idx: number) => { const indikatorList = (sasaran.children || []).map( (child: any) => ({ uraian: child.uraian || '-', satuan: child.satuan || '-', target: child.target ?? '-', }), ); return [ idx + 1, sasaran.uraian || '-', indikatorList.length > 0 ? indikatorList .map((ind) => ind.uraian) .join('\n\n') : '-', indikatorList.length > 0 ? indikatorList .map((ind) => `${ind.target}${ind.satuan}`) .join('\n\n') : '-', ]; }); // tabel sasaran autoTable(doc, { startY: 50, head: [ ['No', 'Sasaran Strategis', 'Indikator Kinerja', 'Target'], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: sasaranRows, theme: 'grid', styles: { fontSize: 9 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 80 }, 2: { halign: 'center' }, 3: { cellWidth: 25, halign: 'center' }, }, }); const rows: any[] = []; let grandTotal = 0; let progNo = 1; // ✅ Nomor urut program (item.programs || []).forEach((program: any) => { let totalProgram = 0; // Pastikan program.kegiatans selalu array const kegiatans = Array.isArray(program.kegiatans) ? program.kegiatans : Object.values(program.kegiatans || {}); kegiatans.forEach((kegiatan: any) => { const subkegiatans = Array.isArray(kegiatan.subkegiatans) ? kegiatan.subkegiatans : Object.values(kegiatan.subkegiatans || {}); subkegiatans.forEach((sub: any) => { const anggaran = parseFloat( sub.anggaran?.toString() || '0', ); totalProgram += anggaran; }); }); grandTotal += totalProgram; rows.push([ progNo++, // ✅ Kolom nomor program.nama || '-', { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(totalProgram), styles: { halign: 'right' }, }, ]); }); autoTable(doc, { startY: doc.lastAutoTable?.finalY ? doc.lastAutoTable.finalY + 10 : 60, head: [['No', 'Program', 'Total Anggaran']], // ✅ Tambahkan kolom "No" headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: rows.length > 0 ? rows : [['-', '-', '-']], foot: [ [ { content: 'TOTAL', colSpan: 2, styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(grandTotal), styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, ], ], theme: 'grid', styles: { fontSize: 10 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 120 }, 2: { halign: 'right' }, }, }); let afterProgramY = doc.lastAutoTable?.finalY || 80; // === Tanda tangan Halaman 2 autoTable(doc, { startY: afterProgramY + 10, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ 'BUPATI BANGGAI', '', { content: 'Kepala BKPSDM Kabupaten Banggai', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: 'Ir. H. Amirudin, MM', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ '', '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } else if (item.tingkat === 'eselon_3') { const sasaranRows: any[] = []; let sasaranNo = 1; const selectedSakipIds = ( (item as any).sakip_node_ids || (item.sakipNodes || []).map((sn: any) => sn.id) ).map((id: any) => id.toString()); // Variabel untuk melacak sub nomor kegiatan dalam satu program let subCounter = 1; (item.sakipNodes || []) .filter((sn) => (getSakipKategoriByTingkat(item.tingkat) || []).includes( sn.kategori, ), ) .forEach((sasaran: any) => { const indikatorList = (sasaran.children || []) .filter((child: any) => { if (!selectedSakipIds.length) return true; return selectedSakipIds.includes(String(child.id)); }) .map((child: any) => ({ uraian: child.uraian, satuan: child.satuan || '-', target: child.target || '-', })); const isProgram = sasaran.kategori === 'sasaran program'; const isKegiatan = sasaran.kategori === 'sasaran kegiatan'; // Jika ketemu program baru, reset subCounter if (isProgram) { subCounter = 1; } if (indikatorList.length > 0) { if (isProgram) { indikatorList.forEach( (indikator: any, idx: number) => { sasaranRows.push([ idx === 0 ? sasaranNo : '', idx === 0 ? sasaran.uraian || '-' : '', indikator.uraian, indikator.satuan, indikator.target, ]); }, ); sasaranNo++; } else if (isKegiatan) { const subNo = `${sasaranNo - 1}.${subCounter++}`; // gunakan nomor program sebelumnya indikatorList.forEach( (indikator: any, idx: number) => { sasaranRows.push([ idx === 0 ? subNo : '', idx === 0 ? sasaran.uraian || '-' : '', indikator.uraian, indikator.satuan, indikator.target, ]); }, ); } } else { if (isProgram) { sasaranRows.push([ sasaranNo++, sasaran.uraian || '-', '-', '-', '-', ]); } else if (isKegiatan) { const subNo = `${sasaranNo - 1}.${subCounter++}`; sasaranRows.push([ subNo, sasaran.uraian || '-', '-', '-', '-', ]); } } }); // === Cetak tabel sasaran autoTable(doc, { startY: 50, head: [ [ 'No', 'Sasaran Program / Kegiatan', 'Indikator Sasaran', 'Satuan', 'Target', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: sasaranRows, theme: 'grid', styles: { fontSize: 9 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 60 }, 2: { cellWidth: 55 }, 3: { cellWidth: 20, halign: 'center' }, 4: { halign: 'center' }, }, }); // === Hitung dan cetak anggaran const startAnggaranY = doc.lastAutoTable?.finalY ? doc.lastAutoTable.finalY + 10 : 60; const programKegiatanRows: any[] = []; let programNo = 1; let grandTotal = 0; const selectedKegiatanIds = (item.kegiatans || []).map((k: any) => k.id ? k.id.toString() : String(k), ); const selectedSubkegIds = (item.subkegiatans || []).map((s: any) => s.id ? s.id.toString() : String(s), ); const globalCountedSubs = new Set(); const uniquePrograms = (item.programs || []).filter( (prog: any, i: number, arr: any[]) => prog?.id && i === arr.findIndex((p) => String(p.id) === String(prog.id)), ); uniquePrograms.forEach((program: any) => { let totalProgram = 0; let kegiatanNo = 1; const tempKegiatanRows: any[] = []; const kegiatans = Array.isArray(program.kegiatans) ? program.kegiatans : Object.values(program.kegiatans || {}); const filteredKegiatans = kegiatans.filter((kegiatan: any) => { const idStr = kegiatan.id ? kegiatan.id.toString() : String(kegiatan); if (selectedKegiatanIds.length === 0) return true; return selectedKegiatanIds.includes(idStr); }); const uniqueKegiatans = filteredKegiatans.filter( (keg: any, i: number, arr: any[]) => keg?.id && i === arr.findIndex( (k) => String(k.id) === String(keg.id), ), ); uniqueKegiatans.forEach((kegiatan: any) => { let totalKegiatan = 0; const subkegiatans = Array.isArray(kegiatan.subkegiatans) ? kegiatan.subkegiatans : Object.values(kegiatan.subkegiatans || {}); const filteredSubkegiatans = selectedSubkegIds.length ? subkegiatans.filter((sub: any) => selectedSubkegIds.includes(String(sub.id)), ) : subkegiatans; const uniqueSubs = filteredSubkegiatans.filter( (sub: any, i: number, arr: any[]) => sub?.id && i === arr.findIndex( (s) => String(s.id) === String(sub.id), ), ); // Hitung total anggaran dari subkegiatan unik uniqueSubs.forEach((sub: any) => { if (!globalCountedSubs.has(String(sub.id))) { const anggaran = typeof sub.anggaran === 'number' ? sub.anggaran : Number(sub.anggaran) || 0; totalKegiatan += anggaran; globalCountedSubs.add(String(sub.id)); } }); if ( uniqueSubs.length > 0 || selectedSubkegIds.length === 0 ) { totalProgram += totalKegiatan; tempKegiatanRows.push([ { content: `${programNo}.${kegiatanNo++}.`, styles: { fontStyle: 'normal' }, }, { content: kegiatan.nama || '-' }, { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(totalKegiatan), styles: { halign: 'right', fontStyle: 'normal', }, }, ]); } }); if ( tempKegiatanRows.length > 0 || selectedKegiatanIds.length === 0 ) { if (totalProgram > 0) grandTotal += totalProgram; programKegiatanRows.push([ { content: `${programNo++}.`, styles: { fontStyle: 'normal' }, }, { content: `${program.nama || '-'}`, styles: { fontStyle: 'normal' }, }, { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(totalProgram), styles: { halign: 'right', fontStyle: 'normal' }, }, ]); programKegiatanRows.push(...tempKegiatanRows); } }); autoTable(doc, { startY: startAnggaranY, head: [['No', 'Uraian', 'Total Anggaran']], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: programKegiatanRows.length > 0 ? programKegiatanRows : [ [ '-', { content: '-', styles: { halign: 'right' } }, ], ], foot: [ [ { content: 'TOTAL', colSpan: 2, styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(grandTotal), styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, ], ], theme: 'grid', styles: { fontSize: 10 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 120 }, 2: { halign: 'right' }, }, }); // === Tanda tangan const afterProgramY = doc.lastAutoTable?.finalY || 80; autoTable(doc, { startY: afterProgramY + 10, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ { content: item.jabatan_kedua || '-', styles: { halign: 'center' }, }, '', { content: item.asn?.jabatan || '-', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: item.nama_kedua || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ { content: item.nip_kedua ? `NIP. ${item.nip_kedua}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } else if (item.tingkat === 'eselon_4') { const sasaranRows: any[] = []; let sasaranNo = 1; (item.sakipNodes || []) .filter( (sn: SakipNode) => sn.kategori === getSakipKategoriByTingkat(item.tingkat), ) .forEach((sasaran: SakipNode) => { const indikatorList = (sasaran.children || []).map( (child: any) => ({ uraian: child.uraian, satuan: child.satuan || '-', target: child.target || '-', }), ); if (indikatorList.length > 0) { sasaranRows.push([ { content: sasaranNo++, rowSpan: indikatorList.length, styles: { valign: 'top' }, }, { content: sasaran.uraian || '-', rowSpan: indikatorList.length, styles: { valign: 'top', fontStyle: 'bold' }, }, indikatorList[0].uraian, indikatorList[0].satuan, indikatorList[0].target, ]); for (let i = 1; i < indikatorList.length; i++) { sasaranRows.push([ indikatorList[i].uraian, indikatorList[i].satuan, indikatorList[i].target, ]); } } else { sasaranRows.push([ sasaranNo++, sasaran.uraian || '-', '-', '-', '-', ]); } }); autoTable(doc, { startY: 50, head: [ [ 'No', 'Sasaran Sub Kegiatan', 'Indikator Kinerja', 'Satuan', 'Target', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: sasaranRows, theme: 'grid', styles: { fontSize: 9 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 60 }, 2: { cellWidth: 55 }, 3: { cellWidth: 20, halign: 'center' }, 4: { halign: 'center' }, }, }); // === Tabel Anggaran Sub Kegiatan === const subKegiatanRows: any[] = []; let subNo = 1; // Titik awal tabel Anggaran const startAnggaranY = doc.lastAutoTable?.finalY ? doc.lastAutoTable.finalY + 10 : 60; // Pastikan item.subkegiatans adalah array const subkegiatans = Array.isArray(item.subkegiatans) ? item.subkegiatans : Object.values(item.subkegiatans || {}); // Hitung Grand Total const grandTotal = subkegiatans.reduce((sum: number, sub: any) => { const nominal = parseFloat(sub?.anggaran?.toString() || '0'); return sum + (isNaN(nominal) ? 0 : nominal); }, 0); // Buat baris-baris tabel Sub Kegiatan subkegiatans.forEach((sub: any) => { const anggaran = parseFloat(sub?.anggaran?.toString() || '0'); if (!sub?.nama) return; subKegiatanRows.push([ { content: subNo++, styles: { halign: 'center' }, }, sub.nama || '-', { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(isNaN(anggaran) ? 0 : anggaran), styles: { halign: 'right' }, }, ]); }); autoTable(doc, { startY: startAnggaranY, head: [['No', 'Uraian Sub Kegiatan', 'Anggaran']], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: subKegiatanRows.length > 0 ? subKegiatanRows : [ [ '-', '-', { content: '-', styles: { halign: 'right' } }, ], ], foot: [ [ { content: 'TOTAL', colSpan: 2, styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, { content: Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0, }).format(grandTotal), styles: { halign: 'right', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, }, ], ], theme: 'grid', styles: { fontSize: 10 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 120 }, 2: { halign: 'right' }, }, }); const afterProgramY = doc.lastAutoTable?.finalY || 80; // === Tanda Tangan === autoTable(doc, { startY: afterProgramY + 10, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ { content: item.jabatan_kedua || '-', styles: { halign: 'center' }, }, '', { content: item.asn?.jabatan || '-', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: item.nama_kedua || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ { content: item.nip_kedua ? `NIP. ${item.nip_kedua}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } else if (item.tingkat === 'fungsional') { const sasaranRows: any[] = []; let sasaranNo = 1; (item.sakipNodes || []) .filter( (sn: SakipNode) => sn.kategori === getSakipKategoriByTingkat(item.tingkat), ) .forEach((sasaran: SakipNode) => { const indikatorList = (sasaran.children || []).map( (child: any) => ({ uraian: child.uraian, satuan: child.satuan || '-', target: child.target || '-', }), ); if (indikatorList.length > 0) { sasaranRows.push([ { content: sasaranNo++, rowSpan: indikatorList.length, styles: { valign: 'top' }, }, { content: sasaran.uraian || '-', rowSpan: indikatorList.length, styles: { valign: 'top', fontStyle: 'bold' }, }, indikatorList[0].uraian, indikatorList[0].satuan, indikatorList[0].target, ]); for (let i = 1; i < indikatorList.length; i++) { sasaranRows.push([ indikatorList[i].uraian, indikatorList[i].satuan, indikatorList[i].target, ]); } } else { sasaranRows.push([ sasaranNo++, sasaran.uraian || '-', '-', '-', '-', ]); } }); autoTable(doc, { startY: 50, head: [ [ 'No', 'Sasaran Sub Kegiatan', 'Indikator Kinerja', 'Satuan', 'Target', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: sasaranRows, theme: 'grid', styles: { fontSize: 9 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 60 }, 2: { cellWidth: 55 }, 3: { cellWidth: 20, halign: 'center' }, 4: { halign: 'center' }, }, }); const startTtdY = doc.lastAutoTable?.finalY ? doc.lastAutoTable.finalY + 10 : 60; // === Tanda Tangan === autoTable(doc, { startY: startTtdY, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ { content: item.jabatan_kedua || '-', styles: { halign: 'center' }, }, '', { content: item.asn?.jabatan || '-', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: item.nama_kedua || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ { content: item.nip_kedua ? `NIP. ${item.nip_kedua}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } else if (item.tingkat === 'staf') { const sasaranRows: any[] = []; let sasaranNo = 1; (item.sakipNodes || []) .filter( (sn: SakipNode) => sn.kategori === getSakipKategoriByTingkat(item.tingkat), ) .forEach((sasaran: SakipNode) => { const indikatorList = (sasaran.children || []).map( (child: any) => ({ uraian: child.uraian, satuan: child.satuan || '-', target: child.target || '-', }), ); if (indikatorList.length > 0) { sasaranRows.push([ { content: sasaranNo++, rowSpan: indikatorList.length, styles: { valign: 'top' }, }, { content: sasaran.uraian || '-', rowSpan: indikatorList.length, styles: { valign: 'top', fontStyle: 'bold' }, }, indikatorList[0].uraian, indikatorList[0].satuan, indikatorList[0].target, ]); for (let i = 1; i < indikatorList.length; i++) { sasaranRows.push([ indikatorList[i].uraian, indikatorList[i].satuan, indikatorList[i].target, ]); } } else { sasaranRows.push([ sasaranNo++, sasaran.uraian || '-', '-', '-', '-', ]); } }); autoTable(doc, { startY: 50, head: [ [ 'No', 'Sasaran Sub Kegiatan', 'Indikator Kinerja', 'Satuan', 'Target', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', lineWidth: 0.1, textColor: [0, 0, 0], fillColor: [255, 255, 255], }, body: sasaranRows, theme: 'grid', styles: { fontSize: 9 }, columnStyles: { 0: { cellWidth: 15, halign: 'center' }, 1: { cellWidth: 60 }, 2: { cellWidth: 55 }, 3: { cellWidth: 20, halign: 'center' }, 4: { halign: 'center' }, }, }); const startTtdY = doc.lastAutoTable?.finalY ? doc.lastAutoTable.finalY + 10 : 60; // === Tanda Tangan === autoTable(doc, { startY: startTtdY, body: [ [ '', '', { content: 'Luwuk, 10 Januari 2025', styles: { halign: 'center' }, }, ], [ 'Pihak Kedua,', '', { content: 'Pihak Pertama,', styles: { halign: 'center' }, }, ], [ { content: item.jabatan_kedua || '-', styles: { halign: 'center' }, }, '', { content: item.asn?.jabatan || '-', styles: { halign: 'center' }, }, ], [ { content: '', colSpan: 3, styles: { minCellHeight: 25 }, }, ], [ { content: item.nama_kedua || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, '', { content: item.asn?.nama || '-', styles: { halign: 'center', fontStyle: 'bold' }, }, ], [ { content: item.nip_kedua ? `NIP. ${item.nip_kedua}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, '', { content: item.asn?.nip ? `NIP. ${item.asn.nip}` : 'NIP. -', styles: { cellPadding: { top: -1 } }, }, ], ], theme: 'plain', styles: { fontSize: 11, halign: 'center' }, margin: { left: 22 }, didDrawCell: (data) => { if ( data.row.index === 4 && (data.column.index === 0 || data.column.index === 2) ) { const { x, y, height } = data.cell; const text = data.cell.text[0]; const textWidth = doc.getTextWidth(text); const startX = x + (data.cell.width - textWidth) / 2; const endX = startX + textWidth; doc.setLineWidth(0.2); doc.setDrawColor(0, 0, 0); doc.line(startX, y + height - 2, endX, y + height - 2); } }, }); } // === Generate nama file dinamis const nama = item.asn?.nama ? item.asn.nama.replace(/\s+/g, '-') : 'Tanpa-Nama'; const nip = item.asn?.nip || 'Tanpa-NIP'; const fileName = `Perjanjian-Kinerja-${nama}-${nip}.pdf`; // === Preview ke Modal const pdfBlob = doc.output('blob'); const url = URL.createObjectURL(pdfBlob); setPdfUrl(url); setShowPdfModal(true); // === Optional: Download otomatis // doc.save(fileName); }; const handleDownload = () => { if (!pdfUrl) return; const link = document.createElement('a'); link.href = pdfUrl; link.download = 'Perjanjian-Kinerja.pdf'; link.click(); }; const renderSelectedNames = (selectedIds: string[], list: any[]) => { if (!selectedIds || selectedIds.length === 0) return ''; return list .filter((item) => selectedIds.includes(item.id.toString())) .map((item) => item.nama) .join(', '); }; useEffect(() => { if (!form?.data) return; // Fungsi sinkronisasi aman dengan fallback const syncIfArray = (key: string, setter: (v: string[]) => void) => { const value = (form.data as any)?.[key]; // 1. Jika value adalah array of string (Ideal dari toggleMultiSelect) if ( Array.isArray(value) && value.every((v) => typeof v === 'string') ) { setter(value); return; } // 2. Jika value adalah array of number/object (dari backend atau form.setData) if (Array.isArray(value)) { // Konversi array of {id: number} atau array of number menjadi array of string ID const arr = value .map((v) => { if (typeof v === 'object' && v !== null && 'id' in v) { return String(v.id); } return String(v); }) .filter((v) => v !== 'null' && v !== 'undefined'); // Hapus nilai yang tidak valid setelah konversi setter(arr); return; } // 3. Jika bukan array, reset state setter([]); }; // Sinkronisasi semua field multi-select syncIfArray('sakip_node_ids', setSelectedSakipNodes); syncIfArray('program_ids', setSelectedPrograms); syncIfArray('kegiatan_ids', setSelectedKegiatans); syncIfArray('subkegiatan_ids', setSelectedSubKegiatans); }, [form.data]); return (

Perjanjian Kinerja

Halaman ini digunakan untuk mengelola perjanjian kinerja

setSearchQuery(e.target.value)} />
NO NAMA LEVEL {/* Perbaikan typo class: text-centeruppercase menjadi text-center uppercase */} {/* Menggunakan IIFE untuk menghitung list yang difilter dan menerapkan operator ternary */} {(() => { // 1. Hitung filtered list (logika filter tetap sama) const filteredPkList = pkList.filter((item) => { const query = searchQuery.toLowerCase(); if (query === '') return true; const nameMatch = item.asn?.nama ?.toLowerCase() .includes(query); const nipMatch = item.asn?.nip ?.toLowerCase() .includes(query); const uraianMatch = item.uraian ?.toLowerCase() .includes(query); return nameMatch || nipMatch || uraianMatch; }); // 2. Terapkan Ternary Operator: Jika list hasil filter > 0, tampilkan hasil map. Jika tidak, tampilkan pesan fallback. return filteredPkList.length > 0 ? ( filteredPkList.map((item, index) => ( {index + 1}
{item.asn?.nama ?.split(' ') .slice(0, 2) .map( (word) => word[0], ) .join('') .toUpperCase()}
{/* Nama dan NIP */}
{item.asn?.nama} {item.asn?.nip}
{item.tingkat === 'eselon_2' ? 'Eselon II' : item.tingkat === 'eselon_3' ? 'Eselon III' : item.tingkat === 'eselon_4' ? 'Eselon IV' : item.tingkat === 'staf' ? 'Staf' : item.tingkat === 'fungsional' ? 'Fungsional' : item.tingkat}

Preview

Edit

Hapus

Hapus Data Apakah Anda yakin ingin menghapus Indikator Kinerja Individu{' '} { item .asn ?.nama } ?
Batal { handleDelete( item.id, ); setShowDeleteAlert( false, ); }} className="bg-red-500 text-white hover:bg-red-600" > Hapus
)) ) : ( // Pesan fallback (TIDAK ADA DATA) {searchQuery !== '' ? ( Hasil Pencarian Nama atau NIP yang kamu cari tidak ditemukan...🤣 ) : ( Masih Kosong Silahkan input data terlebih dahulu. )} ); })()}
{ setShowFormModal(open); if (!open) resetFormState(); }} > {isEdit ? 'Edit Perjanjian Kinerja' : 'Tambah Perjanjian Kinerja'}
{/* ASN */}
setOpenPopover((prev) => ({ ...prev, asn: open, })) } > e.stopPropagation()} > Tidak ada ASN {asnList.map((asn) => ( { form.setData( 'asn_id', asn.id.toString(), ); setOpenPopover( ( prev, ) => ({ ...prev, asn: false, }), ); }} >

{ asn.nama }

{ asn.nip }

))}
{/* Tahun */}
{/* Tingkat */}
{form.data.tingkat && (
setOpenPopover((prev) => ({ ...prev, sakip: open, })) } > e.stopPropagation() } > Tidak ada sasaran {sakipNodeList .filter((sn) => ( getSakipKategoriByTingkat( form .data .tingkat, ) || [] ).includes( sn.kategori, ), ) .map((s) => { const isSelected = selectedSakipNodes.includes( s.id.toString(), ); return ( { toggleMultiSelect( selectedSakipNodes, setSelectedSakipNodes, s.id.toString(), 'sakip_node_ids', ); }} className="w-full break-words whitespace-normal" > {/* ✅ FIX: Ganti input checkbox dengan ikon Check */}

{ s.uraian }

( { s.kategori } )

); })}
)} {form.data.tahun_id && [ 'eselon_2', 'eselon_3', 'eselon_4', 'staf', 'fungsional', ].includes(form.data.tingkat) && (
setOpenPopover((prev) => ({ ...prev, program: open, })) } > e.stopPropagation() } > Tidak ada program {programList .filter( (p) => p.tahun_id.toString() === form .data .tahun_id, ) .map( (p) => { const isSelected = selectedPrograms.includes( p.id.toString(), ); return ( toggleMultiSelect( selectedPrograms, setSelectedPrograms, p.id.toString(), 'program_ids', ) } className="w-full break-words whitespace-normal" > { p.nama } ); }, )}
)} {selectedPrograms.length > 0 && [ 'eselon_3', 'eselon_4', 'staf', 'fungsional', ].includes(form.data.tingkat) && (
setOpenPopover((prev) => ({ ...prev, kegiatan: open, })) } > e.stopPropagation() } > Tidak ada kegiatan {kegiatanList .filter( (k) => // 1. Kegiatan yang terkait Program yang aktif selectedPrograms.includes( k.program_id.toString(), ) || // 2. 🎯 FIX: Kegiatan yang sudah terpilih (terutama saat edit) selectedKegiatans.includes( k.id.toString(), ), ) .map( (k) => { const isSelected = selectedKegiatans.includes( k.id.toString(), ); return ( toggleMultiSelect( selectedKegiatans, setSelectedKegiatans, k.id.toString(), 'kegiatan_ids', ) } className="w-full break-words whitespace-normal" > {/* Menjamin tanda centang tampil */} { k.nama } ); }, )}
)} {selectedKegiatans.length > 0 && [ 'eselon_3', 'eselon_4', 'staf', 'fungsional', ].includes(form.data.tingkat) && (
setOpenPopover((prev) => ({ ...prev, subkegiatan: open, })) } > e.stopPropagation() } > Tidak ada sub kegiatan {subkegList .filter( (s) => selectedKegiatans.includes( s.kegiatan_id.toString(), ) || // Pertahankan filter lama untuk melihat semua yang pernah terpilih/berkaitan selectedSubKegiatans.includes( s.id.toString(), ) || form.data.subkegiatan_ids?.includes( s.id.toString(), ), ) .map( (s) => { const isSelected = selectedSubKegiatans.includes( s.id.toString(), ); return ( toggleMultiSelect( selectedSubKegiatans, setSelectedSubKegiatans, s.id.toString(), 'subkegiatan_ids', ) } className="w-full break-words whitespace-normal" > {/* ✅ FIX: Ganti input checkbox dengan ikon Check */} { s.nama } ); }, )}
)} {/* Pihak Kedua */} {[ 'eselon_3', 'eselon_4', 'fungsional', 'staf', ].includes(form.data.tingkat) && (
setOpenPopover((prev) => ({ ...prev, pihakKedua: open, })) } > e.stopPropagation() } > Tidak ada ASN {asnList.map( (asn) => ( { // Saat ASN dipilih → isi otomatis ke input form.setData( { ...form.data, asn_kedua_id: asn.id.toString(), nama_kedua: asn.nama, nip_kedua: asn.nip, jabatan_kedua: asn.jabatan, }, ); setOpenPopover( ( prev, ) => ({ ...prev, pihakKedua: false, }), ); }} >

{ asn.nama }

{ asn.nip }

), )}
{/* Input otomatis terisi tapi masih bisa diedit */}
form.setData( 'nama_kedua', e.target.value, ) } className="col-span-3" />
form.setData( 'nip_kedua', e.target.value, ) } className="col-span-3" />
form.setData( 'jabatan_kedua', e.target.value, ) } className="col-span-3" />
)}
Preview PK Gunakan tombol unduh jika Anda ingin menyimpan dokumen ini.
{pdfUrl ? (