(() => { const { Card, Badge, Button, Icon, Avatar, Input, Select, Field, StatusPill, Checkbox, Modal, Textarea } = window.TaskFlowDesignSystem_dd53c2; /* Document — arsip dokumen yang sudah diterima (accepted) atau sudah ditandatangani. Admin melihat seluruh dokumen; user hanya dokumen dari task miliknya sendiri. Server WAJIB memfilter ulang per user_id (cegah IDOR pada endpoint download). */ const CATS = ["Semua Kategori", "Dokumen", "Operasional", "Kepegawaian", "Kegiatan", "Sarana & Prasarana", "Keuangan"]; const KINDS = ["Semua Dokumen", "Sudah di-TTD", "Tanpa TTD"]; const PAGE = 20; function docsFrom(tasks, userId) { const out = []; tasks.forEach((t) => { if (t.status !== "done") return; // hanya task yang sudah diterima if (userId != null && userId !== 0 && t.user !== userId) return; const files = t.evidences && t.evidences.length ? t.evidences : t.evidence ? [t.evidence] : []; files.forEach((f, i) => out.push({ key: t.id + "-" + i, task: t, file: f, signed: !!(t.needSign && t.signature), round: t.versions && t.versions.length ? t.versions.length + 1 : 1, })); }); return out; } function DocumentScreen({ tasks, userId, onView, onViewSignature, onTimeline, onSign, onDelete }) { const [q, setQ] = React.useState(""); const [cat, setCat] = React.useState(CATS[0]); const [kind, setKind] = React.useState(KINDS[0]); const [shown, setShown] = React.useState(PAGE); const [sel, setSel] = React.useState([]); const [zipping, setZipping] = React.useState(false); const [note, setNote] = React.useState(""); /* Hapus dokumen final — hanya Direktur, wajib beralasan, task dikembalikan ke Need Revision agar user mengunggah dokumen pengganti (jejaknya tetap di Log Activity). */ const [del, setDel] = React.useState(null); const [delReason, setDelReason] = React.useState(""); const [delErr, setDelErr] = React.useState(""); const isAdmin = userId == null || userId === 0; const all = React.useMemo(() => docsFrom(tasks, userId), [tasks, userId]); const rows = React.useMemo(() => all.filter((d) => { const s = q.trim().toLowerCase(); if (s && !(d.file.name.toLowerCase().includes(s) || d.task.type.toLowerCase().includes(s) || nameOf(d.task.user).toLowerCase().includes(s))) return false; if (cat !== CATS[0] && d.task.category !== cat) return false; if (kind === KINDS[1] && !d.signed) return false; if (kind === KINDS[2] && d.signed) return false; return true; }), [all, q, cat, kind]); const page = rows.slice(0, shown); const fmtKb = (n) => `${Math.max(1, Math.round(n / 1024))} KB`; const toggle = (k) => setSel((s) => (s.includes(k) ? s.filter((x) => x !== k) : [...s, k])); /* Bulk ZIP: byte asli dipakai bila ada; dokumen contoh tanpa byte diberi berkas keterangan agar arsip tetap konsisten. Diproses berurutan supaya memori stabil. */ const downloadZip = async () => { const picked = rows.filter((r) => sel.includes(r.key)); if (!picked.length || !window.makeZip) return; setZipping(true); try { const files = []; for (const p of picked) { const bytes = await window.fileBytes(p.file); const base = (p.signed ? p.file.name.replace(/\.pdf$/i, "") + "-ttd.pdf" : p.file.name); files.push(bytes ? { name: base, data: bytes } : { name: base.replace(/\.[a-z0-9]+$/i, "") + ".txt", data: `${p.file.name}\nTask: ${p.task.type}\nUser: ${nameOf(p.task.user)}\nKategori: ${p.task.category}\nDiterima: ${p.task.done}\nTTD: ${p.signed ? "ya" : "tidak"}\n` }); } window.saveBlob(window.makeZip(files), `arsip-dokumen-${files.length}-berkas.zip`); setNote(`${files.length} dokumen diunduh sebagai ZIP.`); setSel([]); } finally { setZipping(false); } }; // Dokumen ber-TTD diunduh dengan tanda tangan menyatu di halamannya. const dl = (d) => { if (d.signed && d.file.url && window.downloadSigned) { return window.downloadSigned(d.file, DB.signature, d.task.signature, d.file.name.replace(/(\.[a-z0-9]+)?$/i, "-ttd$1")); } if (d.file.url && window.downloadSigned) return window.downloadSigned(d.file, null, null, d.file.name); return window.downloadEvidence && window.downloadEvidence( d.signed ? { name: d.file.name.replace(/\.pdf$/i, "") + "-ttd.pdf", size: d.file.size || 480000, compressed: d.file.compressed || 420000 } : d.file, d.task.type + (d.signed ? " · sudah ditandatangani Direktur" : " · dokumen final")); }; const stat = (label, value, tone) => (
{label} {value}
); return (
{stat("Total dokumen", all.length)} {stat("Sudah di-TTD", all.filter((d) => d.signed).length, "var(--leaf-600)")} {stat("Tanpa TTD", all.filter((d) => !d.signed).length, "var(--teal-700)")} {isAdmin && stat("Pemilik dokumen", new Set(all.map((d) => d.task.user)).size)}

Arsip Dokumen

{rows.length} {rows.length > 0 && ( 0} onChange={() => setSel(sel.length === rows.length ? [] : rows.map((r) => r.key))} /> )} {isAdmin ? "Semua dokumen dari task yang sudah diterima." : "Dokumen dari task milik Anda yang sudah diterima Direktur."}
{note && {note}}
{ setQ(e.target.value); setShown(PAGE); }} /> { setKind(e.target.value); setShown(PAGE); }} options={KINDS} />
{page.length === 0 ? (
Belum ada dokumen Dokumen muncul di sini setelah task diterima{isAdmin ? "" : " Direktur"}, termasuk dokumen yang sudah ditandatangani.
) : (
{page.map((d) => { const u = userById(d.task.user); return (
toggle(d.key)} /> {d.file.name} {d.task.type} · {fmtKb(d.file.compressed || d.file.size || 480000)} · diterima {d.task.done || "-"} {u.name} {d.task.category} {d.signed ? Sudah di-TTD : Tanpa TTD} {d.round > 1 && v{d.round}} {onTimeline && } {/* Dokumen wajib TTD yang belum ditandatangani masih bisa ditandatangani dari sini. */} {onSign && d.task.needSign && !d.signed && } {isAdmin && onDelete && ( )}
); })} {rows.length > page.length && ( )}
)} {del && ( setDel(null)} description="Dokumen dihapus permanen dan task dikembalikan ke Need Revision agar user mengunggah dokumen yang benar." footer={<> }>
{del.file.name} {del.task.type} · {nameOf(del.task.user)}{del.signed ? " · tanda tangan ikut terhapus" : ""}