(() => { const { Modal, Field, Input, Select, Button, Checkbox, Badge, Textarea, FileUpload, IconButton, Icon } = window.TaskFlowDesignSystem_dd53c2; const MONTHS_ID = ["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"]; const MODAL_ISO_TODAY = window.ISO_TODAY || (() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; })(); const fmtDate = (iso) => { const [y,m,d] = iso.split("-"); return `${d} ${MONTHS_ID[+m-1]} ${y}`; }; const toIso = (id) => { if (!id) return MODAL_ISO_TODAY; const [d, m, y] = id.split(" "); return `${y}-${String(MONTHS_ID.indexOf(m) + 1).padStart(2, "0")}-${d.padStart(2, "0")}`; }; /* Pasted screenshots become evidence files without a trip through the file picker (PRD §5.4). The mock keeps the same shape FileUpload produces. */ let pasteSeq = 0; const fromPastedImage = (file, maxKb = 500) => ({ name: file.name && file.name !== "image.png" ? file.name : `screenshot-${++pasteSeq}.png`, size: file.size, compressed: file.size, ok: true, kind: "image", pasted: true, type: file.type || "image/png", }); /* Isi screenshot dibaca sungguhan lalu dikompres (maks 1600px) supaya bisa dipratinjau dan tersimpan, sama seperti file yang diunggah lewat tombol upload. */ const readPastedImage = (file, maxKb = 500) => new Promise((res) => { const meta = fromPastedImage(file, maxKb); const rd = new FileReader(); rd.onload = () => { const img = new Image(); img.onload = () => { const sc = Math.min(1, 1600 / Math.max(img.width, img.height)); const c = document.createElement("canvas"); c.width = Math.max(1, Math.round(img.width * sc)); c.height = Math.max(1, Math.round(img.height * sc)); c.getContext("2d").drawImage(img, 0, 0, c.width, c.height); const bytes = (u) => Math.round((u.length - (u.indexOf(",") + 1)) * 0.75); let q = 0.82, out = c.toDataURL("image/jpeg", q); while (bytes(out) > maxKb * 1024 && q > 0.34) { q -= 0.12; out = c.toDataURL("image/jpeg", q); } res({ ...meta, url: out, type: "image/jpeg", compressed: bytes(out) }); }; img.onerror = () => res({ ...meta, url: rd.result }); img.src = rd.result; }; rd.onerror = () => res(meta); rd.readAsDataURL(file); }); function AddTaskModal({ open, onClose, onCreate, mode = "add", initial, tasks = TASKS }) { const isEdit = mode === "edit"; const load = workloadOf(tasks); const loadOf = (id) => load.find((w) => w.id === id) || { load: 0, cap: WORKLOAD_CAP, level: "idle" }; // Jangan pilih user apa pun secara default: id "1" dulu hardcoded sehingga hint // menghitung 1 user terpilih padahal tidak ada checkbox yang tercentang. const [picked, setPicked] = React.useState(initial ? [initial.user] : []); const [type, setType] = React.useState(initial ? initial.type : ""); const [projectName, setProjectName] = React.useState(""); const [rows, setRows] = React.useState([]); const [category, setCategory] = React.useState(initial ? initial.category : "Operasional"); const [priority, setPriority] = React.useState(initial ? initial.priority : "Medium"); const [repeat, setRepeat] = React.useState(initial && initial.repeat ? initial.repeat : "none"); // Sub-task checklist: satu baris = satu sub-task. Pada task berulang, checklist ini // dipakai ulang setiap periode (centangnya direset otomatis). const [needSign, setNeedSign] = React.useState(!!(initial && initial.needSign)); const [subs, setSubs] = React.useState(() => (initial && initial.checklist ? initial.checklist.map((c) => ({ key: "c" + c.id, label: c.label })) : [])); const addSub = () => setSubs((l) => [...l, { key: "c-" + Math.random().toString(36).slice(2, 7), label: "" }]); const setSub = (key, label) => setSubs((l) => l.map((s) => (s.key === key ? { ...s, label } : s))); const removeSub = (key) => setSubs((l) => l.filter((s) => s.key !== key)); // Task baru selalu mulai dari tanggal login; target default seminggu setelahnya. const [assigned, setAssigned] = React.useState(initial ? toIso(initial.assigned) : MODAL_ISO_TODAY); const [target, setTarget] = React.useState(initial ? toIso(initial.target) : isoPlus(7)); const [err, setErr] = React.useState(""); const toggle = (id) => setPicked((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); const isProject = picked.length > 1; // one task row per picked user by default; extra rows can be added for anyone picked React.useEffect(() => { if (picked.length < 2) { setRows([]); return; } setRows((prev) => { const kept = prev.filter((r) => picked.includes(r.user)); const missing = picked.filter((id) => !kept.some((r) => r.user === id)); return [...kept, ...missing.map((id) => ({ key: `r${id}-${Math.random().toString(36).slice(2, 7)}`, user: id, type: "" }))] .sort((a, b) => picked.indexOf(a.user) - picked.indexOf(b.user)); }); }, [picked.join(",")]); const setRow = (key, patch) => setRows((l) => l.map((r) => (r.key === key ? { ...r, ...patch } : r))); const addRow = () => setRows((l) => [...l, { key: `r-${Math.random().toString(36).slice(2, 7)}`, user: picked[0], type: "" }]); const removeRow = (key) => setRows((l) => (l.length > 1 ? l.filter((r) => r.key !== key) : l)); const submit = () => { if (!picked.length) { setErr("Pilih minimal satu user."); return; } if (isProject && !isEdit) { if (!projectName.trim()) { setErr("Nama project wajib diisi."); return; } if (!rows.length || rows.some((r) => !r.type.trim())) { setErr("Setiap task dalam project wajib punya jenis task dan user."); return; } } else if (!type.trim()) { setErr("Jenis task wajib diisi."); return; } if (target < assigned) { setErr("Target tanggal selesai tidak boleh sebelum tanggal diberikan."); return; } setErr(""); onCreate({ type: isProject && !isEdit ? projectName.trim() : type.trim(), projectName: projectName.trim() || undefined, rows: isProject && !isEdit ? rows.map((r) => ({ type: r.type.trim(), user: r.user })) : undefined, category, priority, repeat, users: picked, assigned: fmtDate(assigned), target: fmtDate(target), isProject, needSign, checklist: subs.filter((s) => s.label.trim()).map((s, i) => ({ id: i + 1, label: s.label.trim(), done: false })), }); setType(""); setProjectName(""); setPicked([]); }; return ( }>
{isEdit ? null : (
{USERS.map((u) => { const w = loadOf(u.id); return ( toggle(u.id)} label={`${u.name} · ${u.role}`} /> {w.load} aktif ); })}
)} {isProject && !isEdit ? ( <> setProjectName(e.target.value)} />
{rows.map((r) => (
setRow(r.key, { type: e.target.value })} /> setType(e.target.value)} /> )}
setPriority(e.target.value)} options={["Low","Medium","High"]} />
setAssigned(e.target.value)} /> setTarget(e.target.value)} />
setSub(s.key, e.target.value)} /> removeSub(s.key)} />
))} {subs.length > 0 && {subs.filter((s) => s.label.trim()).length} sub-task{isProject && !isEdit ? " · berlaku untuk semua task di project ini" : ""}}
{picked.some((id) => loadOf(id).level === "over" || loadOf(id).level === "full") && ( Beban kerja penuh: {picked.filter((id) => loadOf(id).level === "over" || loadOf(id).level === "full").map((id) => `${userById(id).name} (${loadOf(id).load} task aktif)`).join(", ")}. Pertimbangkan user lain atau geser targetnya. )}
{isProject ? "Project" : "Single Task"} {picked.length} user terpilih{isProject && !isEdit ? ` · ${rows.length} task` : ""}
{err && {err}}
); } function CompleteTaskModal({ open, task, onClose, onSubmit }) { const [reason, setReason] = React.useState(""); const [note, setNote] = React.useState(""); const [doc, setDoc] = React.useState(null); const [docs, setDocs] = React.useState([]); const [pasted, setPasted] = React.useState(false); const isProjectTask = !!(task && task.project); const late = task ? isOverdue(task.target) : false; const revising = task ? task.status === "revision" : false; const pdfOnly = !!(task && task.needSign); const isPdf = (f) => !!f && f.ok && f.kind !== "image"; // Dokumen hasil unduhan yang sudah ber-TTD ("...-ttd.pdf") tidak boleh diunggah ulang: // tanda tangannya sudah menyatu di halaman, sehingga TTD berikutnya akan menumpuk. const signedName = (f) => !!f && /-ttd(\.[a-z0-9]+)?$/i.test(String(f.name || "")); const docOk = pdfOnly ? (isProjectTask ? docs.some((f) => isPdf(f) && !signedName(f)) : isPdf(doc) && !signedName(doc)) : (isProjectTask ? docs.some((f) => f.ok) : !!(doc && doc.ok)); const reasonOk = !late || reason.trim().length > 0; // paste a screenshot in any notes field → it lands in the evidence slot const attachPasted = async (file) => { if (pdfOnly) return; // task bertanda tangan hanya menerima PDF const f = await readPastedImage(file); if (isProjectTask) setDocs((l) => [...l, f]); else setDoc(f); setPasted(true); }; return ( }>
1. {isProjectTask ? "Minimal satu file; file tambahan opsional." : "Tanpa dokumen, task tidak bisa dikirim untuk verifikasi."} 2. {pdfOnly ? "Task ini butuh tanda tangan Admin, jadi hanya PDF yang diterima — screenshot tidak bisa dipakai." : "Screenshot bisa langsung di-paste (Ctrl+V) pada kolom catatan untuk Admin — otomatis jadi bukti dokumen."} )}> {isProjectTask ? setDocs((l) => [...l, f])} onRemove={(i) => setDocs((l) => l.filter((_, x) => x !== i))} /> : setDoc(null)} />} {pasted && ( Screenshot dari clipboard ditambahkan sebagai bukti dokumen. )} {revising && task.revision && (
Poin revisi dari Admin {task.revision}
)}