import { Button } from '@/components/ui/button'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Textarea } from '@/components/ui/textarea'; import { useAuthBreadcrumbOperator } from '@/hooks/use-auth-breadcrumb-operator'; import AppLayout from '@/layouts/app-layout'; import { cn } from '@/lib/utils'; import { Asn, Iki, SakipNode } from '@/types'; import { Head, router } from '@inertiajs/react'; import { DialogDescription } from '@radix-ui/react-dialog'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; import { Check, ChevronsUpDown, Edit, Eye, FolderCodeIcon, PlusCircle, Search, Trash, } from 'lucide-react'; import { useMemo, useState } from 'react'; interface Props { ikiList: Iki[]; asnList: Asn[]; sakipNodeList: SakipNode[]; } export default function IkiIndex({ ikiList, asnList, sakipNodeList }: Props) { const breadcrumbs = useAuthBreadcrumbOperator(); // 🔍 Search state const [searchQuery, setSearchQuery] = useState(''); // 🧩 Form States const [open, setOpen] = useState(false); const [openAsn, setOpenAsn] = useState(false); const [editMode, setEditMode] = useState(false); const [selectedId, setSelectedId] = useState(null); // sakip node const [openSakip, setOpenSakip] = useState(false); const [sakipNodeId, setSakipNodeId] = useState(''); const [asnId, setAsnId] = useState(''); const [selectedAsn, setSelectedAsn] = useState(null); const [tugas, setTugas] = useState(''); const [fungsi, setFungsi] = useState(['']); const [sakipNodeIds, setSakipNodeIds] = useState([]); const [loading, setLoading] = useState(false); // preview pdf const [openPreview, setOpenPreview] = useState(false); const [pdfUrl, setPdfUrl] = useState(null); const [pdfFileName, setPdfFileName] = useState(''); const filteredSakipNodes = useMemo(() => { if (!selectedAsn) return sakipNodeList; if (selectedAsn.level?.toLowerCase() === 'eselon_2') { return sakipNodeList.filter((n) => { const kategori = n.kategori?.toLowerCase(); return ( kategori === 'sasaran pd' || kategori === 'indikator sasaran pd' ); }); } if (selectedAsn.level?.toLowerCase() === 'eselon_3') { return sakipNodeList.filter((n) => { const kategori = n.kategori?.toLowerCase(); return ( kategori === 'sasaran program' || kategori === 'sasaran kegiatan' || kategori === 'indikator sasaran program' || kategori === 'indikator sasaran kegiatan' ); }); } if (selectedAsn.level?.toLowerCase() === 'eselon_4') { return sakipNodeList.filter( (n) => n.kategori?.toLowerCase() === 'sasaran sub kegiatan', ); } if (selectedAsn.level?.toLowerCase() === 'fungsional') { return sakipNodeList.filter( (n) => n.kategori?.toLowerCase() === 'sasaran sub kegiatan staf', ); } if (selectedAsn.level?.toLowerCase() === 'staf') { return sakipNodeList.filter( (n) => n.kategori?.toLowerCase() === 'sasaran sub kegiatan staf', ); } return sakipNodeList; }, [selectedAsn, sakipNodeList]); // 🔄 Filter pencarian const filteredIkiList = (ikiList ?? []).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); return nameMatch || nipMatch; }); // 🧩 Fungsi Dynamic Input function addFungsi() { setFungsi([...fungsi, '']); } function removeFungsi(index: number) { const newFungsi = [...fungsi]; newFungsi.splice(index, 1); setFungsi(newFungsi); } function handleFungsiChange(index: number, value: string) { const newFungsi = [...fungsi]; newFungsi[index] = value; setFungsi(newFungsi); } // ✳️ Open Modal (Add) function openCreateModal() { setEditMode(false); setSelectedId(null); setAsnId(''); setSelectedAsn(null); setTugas(''); setFungsi(['']); setSakipNodeIds([]); setOpen(true); } // 🟠 Open Modal (Edit) function openEditModal(iki: Iki) { setEditMode(true); setSelectedId(iki.id); setAsnId(String(iki.asn_id)); setSelectedAsn(iki.asn || null); setTugas(iki.tugas); setFungsi(iki.fungsi ?? ['']); if (Array.isArray(iki.sakip_nodes)) { setSakipNodeIds(iki.sakip_nodes.map((n) => n.id.toString())); } else { setSakipNodeIds([]); } setOpen(true); } // 💾 Submit Create/Update async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); const payload = { asn_id: asnId, sakip_node_ids: sakipNodeIds, tugas, fungsi, }; try { if (editMode && selectedId) { router.put(`/a/iki/${selectedId}`, payload, { onSuccess: () => setOpen(false), onFinish: () => setLoading(false), }); } else { router.post('/a/iki', payload, { onSuccess: () => setOpen(false), onFinish: () => setLoading(false), }); } } catch (error) { console.error(error); setLoading(false); } } function handleDelete(id: number) { if (confirm('Yakin ingin menghapus data ini?')) { router.delete(`/a/iki/${id}`); } } // SakipNode multi-select toggle const toggleSakipNode = (id: string) => { if (sakipNodeIds.includes(id)) { setSakipNodeIds(sakipNodeIds.filter((x) => x !== id)); } else { setSakipNodeIds([...sakipNodeIds, id]); } }; function handlePreviewPDF(iki: Iki) { const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', }); const pageWidth = doc.internal.pageSize.getWidth(); let y = 20; // Judul doc.setFont('helvetica', 'bold'); doc.setFontSize(12); doc.text('INDIKATOR KINERJA INDIVIDU', pageWidth / 2, y, { align: 'center', baseline: 'middle', }); // Atur posisi awal y += 15; doc.setFontSize(10); doc.setFont('helvetica', 'normal'); // Posisi dasar kolom const colNomor = 18; let colLabel = 22; let colNomorUrutA = 44; let colTitik = 0; let colIsi = 0; const maxWidth = pageWidth - 20; // Data utama (nomor, label, isi) const data = [ { no: '1.', label: 'Jabatan', value: iki.asn?.jabatan || '-', }, { no: '2.', label: 'Tugas', value: iki.tugas || '-', }, { no: '3.', label: 'Fungsi', value: (iki.fungsi || []) .map((f, i) => `${String.fromCharCode(97 + i)}. ${f}`) .join('\n') || '-', }, ]; // --- Hitung lebar label terpanjang --- let maxLabelWidth = 0; data.forEach((row) => { const labelWidth = doc.getTextWidth(row.label); if (labelWidth > maxLabelWidth) maxLabelWidth = labelWidth; }); // Atur kolom titik dan isi berdasarkan panjang label terpanjang colTitik = colLabel + maxLabelWidth + 3; // titik dua setelah label colIsi = colTitik + 5; // isi teks setelah titik dua data.forEach((row) => { doc.text(row.no, colNomor, y); doc.text(row.label, colLabel, y); doc.text(':', colTitik, y); const isiText = doc.splitTextToSize(row.value, maxWidth - colIsi); doc.text(isiText, colIsi, y); y += isiText.length * 4 + 2; }); if (iki.asn?.level === 'eselon_2') { const selectedNodes = iki.sakip_nodes || []; // 🔹 Ambil semua sasaran program & kegiatan const sasaranList = selectedNodes.filter((sn: SakipNode) => { const kategori = sn.kategori?.toLowerCase(); return kategori === 'sasaran pd'; }); const sasaranRows = sasaranList.map( (sasaran: SakipNode, idx: number) => { const indikatorList = selectedNodes.filter( (child: SakipNode) => child.parent_id === sasaran.id && child.kategori?.toLowerCase() === 'indikator sasaran pd', ); return [ idx + 1, sasaran.uraian || '-', indikatorList.length > 0 ? indikatorList .map((ind) => ind.uraian) .join('\n\n') : '-', sasaran.formulasi || '-', sasaran.sumber_data || '-', ]; }, ); // 🔹 Cetak ke tabel PDF autoTable(doc, { startY: y, head: [ [ 'No', 'Sasaran Program / Kegiatan', 'Indikator Kinerja', 'Formulasi', 'Sumber Data', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', textColor: [0, 0, 0], fillColor: [255, 255, 255], lineWidth: 0.1, }, body: sasaranRows.length > 0 ? sasaranRows : [['', 'Belum ada data sasaran', '', '', '']], theme: 'grid', styles: { fontSize: 9, cellPadding: 3, valign: 'top', textColor: [0, 0, 0], }, columnStyles: { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 55 }, 2: { cellWidth: 50 }, 3: { cellWidth: 40 }, 4: {}, }, }); } else if (iki.asn?.level === 'eselon_3') { const selectedNodes = iki.sakip_nodes || []; // 🔹 Ambil semua sasaran program & kegiatan const sasaranList = selectedNodes.filter((sn: SakipNode) => { const kategori = sn.kategori?.toLowerCase(); return ( kategori === 'sasaran program' || kategori === 'sasaran kegiatan' ); }); const sasaranRows = sasaranList.map( (sasaran: SakipNode, idx: number) => { const indikatorList = selectedNodes.filter( (child: SakipNode) => child.parent_id === sasaran.id && (child.kategori?.toLowerCase() === 'indikator sasaran program' || child.kategori?.toLowerCase() === 'indikator sasaran kegiatan'), ); return [ idx + 1, sasaran.uraian || '-', indikatorList.length > 0 ? indikatorList .map((ind) => ind.uraian) .join('\n\n') : '-', sasaran.formulasi || '-', sasaran.sumber_data || '-', ]; }, ); // 🔹 Cetak ke tabel PDF autoTable(doc, { startY: y, head: [ [ 'No', 'Sasaran Program / Kegiatan', 'Indikator Kinerja', 'Formulasi', 'Sumber Data', ], ], headStyles: { halign: 'center', valign: 'middle', fontStyle: 'bold', textColor: [0, 0, 0], fillColor: [255, 255, 255], lineWidth: 0.1, }, body: sasaranRows.length > 0 ? sasaranRows : [['', 'Belum ada data sasaran', '', '', '']], theme: 'grid', styles: { fontSize: 9, cellPadding: 3, valign: 'top', textColor: [0, 0, 0], }, columnStyles: { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 55 }, 2: { cellWidth: 50 }, 3: { cellWidth: 40 }, 4: {}, }, }); } const offsetX = 30; // jumlah pergeseran ke kanan const centerX = pageWidth / 2 + offsetX; const finalY = doc.lastAutoTable?.finalY || y + 10; const footerY = finalY + 25; // --- Teks bagian footer --- doc.setFontSize(10); doc.setFont('helvetica'); doc.text(iki.asn?.jabatan || '-', centerX, footerY, { align: 'center', }); doc.text( 'Badan Kepegawaian dan Pengembangan SDM', centerX, footerY + 5, { align: 'center', }, ); // --- Nama pejabat --- const name = iki.asn?.nama || '-'; const nameY = footerY + 35; doc.setFont('helvetica', 'bold'); doc.text(name, centerX, nameY, { align: 'center' }); // --- NIP --- doc.setFont('helvetica', 'normal'); const nip = 'NIP.' + iki.asn?.nip || '-'; const nipY = footerY + 41; doc.text(nip, centerX, nipY, { align: 'center' }); // --- Garis bawah berdasarkan panjang NIP --- const nipWidth = doc.getTextWidth(nip); // ambil lebar teks NIP const lineY = nameY + 1.8; // sedikit di bawah nama doc.setLineWidth(0.5); // garis sedikit tebal doc.line(centerX - nipWidth / 2, lineY, centerX + nipWidth / 2, lineY); // Bagian download const nama = iki.asn?.nama?.toUpperCase() || 'TANPA_NAMA'; // Buat nama file const safeNama = nama.replace(/[^\w\s-]/g, '').trim(); const safeNip = nip.replace(/[^\d\s-]/g, '').trim(); const fileName = `IKI-${safeNama} (${safeNip}).pdf`; // Output PDF ke blob untuk preview const blob = doc.output('blob'); const url = URL.createObjectURL(blob); // Kirim ke preview dialog setPdfUrl(url); setPdfFileName(fileName); // <-- tambahkan state ini setOpenPreview(true); } return (

Indikator Kinerja Individu

Halaman ini digunakan untuk mengelola indikator kinerja individu (IKI)

setSearchQuery(e.target.value)} />
{/* Table */}
NO NIP NAMA TAHUN TUGAS FUNGSI AKSI {filteredIkiList.length > 0 ? ( filteredIkiList.map((item, index) => ( {index + 1} {item.asn?.nip} {item.asn?.nama} {item.tahun?.nama} {item.tugas}
    {(item.fungsi ?? []).map( (f, i) => (
  • {f}
  • ), )}
)) ) : ( {searchQuery ? 'Data tidak ditemukan' : 'Belum ada data IKI'} {searchQuery ? 'Coba ubah kata kunci pencarian' : 'Silakan tambah data IKI terlebih dahulu.'} )}
{/* MODAL FORM */} {editMode ? 'Edit IKI' : 'Tambah IKI'}
e.stopPropagation()} > ASN tidak ditemukan. {asnList.map((asn) => ( { const selected = asnList.find( ( a, ) => a.id.toString() === v, ); setAsnId(v); setSelectedAsn( selected || null, ); setOpenAsn( false, ); }} >

{ asn.nama }

( { asn.nip } )

))}
e.stopPropagation()} > Indikator tidak ditemukan. {filteredSakipNodes.map( (node) => ( toggleSakipNode( node.id.toString(), ) } >

{ node.uraian }

( { node.kategori } )

), )}