(() => { const { Card, DataTable, StatusPill, Badge, Button, ProgressBar, Avatar, Tabs, Icon, IconButton } = window.TaskFlowDesignSystem_dd53c2; const MONTH_LABEL = ["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"]; function AdminDashboard({ onOpenAdd, tasks = TASKS, range, onNavigate, onViewSignature, onSignTask }) { const bp = useBreakpoint(); const [tab, setTab] = React.useState("all"); const [lens, setLens] = React.useState("all"); // Derived data is memoized so a big task list is scanned once per change, not per render. const scoped = React.useMemo(() => (range ? tasks.filter((t) => inRange(t.assigned, range.from, range.to)) : tasks), [tasks, range && range.from, range && range.to]); const [shown, setShown] = React.useState(25); React.useEffect(() => { setShown(25); }, [tab, range && range.from, range && range.to]); const lensed = lens === "project" ? scoped.filter((t) => t.project) : scoped; const filtered = React.useMemo(() => scoped.filter((t) => (tab === "all" ? true : t.status === tab)), [scoped, tab]); const pageRows = React.useMemo(() => filtered.slice(0, shown), [filtered, shown]); // capsule bars — task load per 3-day bucket across the period const buckets = []; for (let d = 1; d <= 30; d += 3) { const items = lensed.filter((t) => { const n = parseId(t.assigned).getDate(); return n >= d && n < d + 3; }); buckets.push({ label: String(d).padStart(2, "0"), value: items.length, note: items.length ? `${items.filter((t) => t.status === "done").length} accepted` : null }); } // heat grid — status × weekday density const HEAT_ROWS = ["To Do", "Progress", "Verif", "Accepted"]; const HEAT_KEYS = ["todo", "in_progress", "on_verification", "done"]; const HEAT_COLS = ["S", "S", "R", "K", "J", "S", "M"]; const heat = HEAT_KEYS.map((k) => HEAT_COLS.map((_, i) => lensed.filter((t) => t.status === k && (parseId(t.target).getDay() + 6) % 7 === i).length)); const CATS = ["Dokumen", "Kegiatan", "Sarana & Prasarana", "Kepegawaian", "Operasional"]; const CAT_COLOR = { "Dokumen": "var(--teal-500)", "Kegiatan": "var(--gold-500)", "Sarana & Prasarana": "var(--mint-500)", "Kepegawaian": "var(--coral-500)", "Operasional": "var(--leaf-500)" }; const parts = CATS.map((c) => ({ label: c, value: lensed.filter((t) => t.category === c).length, color: CAT_COLOR[c] })) .filter((p) => p.value > 0); const recent = [...scoped].sort((a, b) => parseId(b.assigned) - parseId(a.assigned)).slice(0, 4); const ranking = React.useMemo(() => rankUsers(tasks, range), [tasks, range && range.from, range && range.to]); const load = React.useMemo(() => workloadOf(scoped), [scoped]); const verifying = scoped.filter((t) => t.status === "on_verification"); /* Tren antar periode: periode terpilih dibandingkan dengan periode sebelumnya yang panjangnya sama. Semua dihitung dalam satu useMemo (dua lintasan array). */ const trend = React.useMemo(() => { if (!range || !range.from || !range.to) return null; const from = new Date(range.from), to = new Date(range.to); const span = Math.max(1, Math.round((to - from) / 86400000) + 1); const shift = (d) => { const x = new Date(d); x.setDate(x.getDate() - span); return x.toISOString().slice(0, 10); }; const prevFrom = shift(from), prevTo = shift(to); const sum = (list) => ({ masuk: list.length, accepted: list.filter((t) => t.status === "done").length, late: list.filter((t) => (t.done ? isOverdue(t.target, t.done) : isOverdue(t.target) && t.status !== "done")).length, avg: list.length ? Math.round(list.reduce((s, t) => s + (t.status === "done" ? 100 : t.progress || 0), 0) / list.length) : 0, }); const now = sum(scoped); const prev = sum(tasks.filter((t) => inRange(t.assigned, prevFrom, prevTo))); return { now, prev, prevFrom, prevTo, span }; }, [tasks, scoped, range && range.from, range && range.to]); /* Target per user: berapa task yang ditargetkan pada periode ini vs yang sudah diterima. */ const targets = React.useMemo(() => USERS.map((u) => { const mine = scoped.filter((t) => t.user === u.id); const done = mine.filter((t) => t.status === "done").length; const late = mine.filter((t) => (t.done ? isOverdue(t.target, t.done) : isOverdue(t.target) && t.status !== "done")).length; return { id: u.id, name: u.name, total: mine.length, done, late, pct: mine.length ? Math.round((done / mine.length) * 100) : 0 }; }).filter((x) => x.total > 0).sort((x, y) => y.pct - x.pct), [scoped]); const accepted = scoped.filter((t) => t.status === "done").length; /* Empat KPI kinerja. Semua dihitung sekali per perubahan data/periode. */ const kpi = React.useMemo(() => { const total = scoped.length; const done = scoped.filter((t) => t.status === "done"); const onTime = done.filter((t) => t.done && !isOverdue(t.target, t.done)).length; const overdue = scoped.filter((t) => (t.done ? isOverdue(t.target, t.done) : t.status !== "done" && isOverdue(t.target))); const resp = scoped.filter((t) => t.started && t.assigned) .map((t) => Math.max(0, Math.round((parseId(t.started) - parseId(t.assigned)) / 86400000))); const fast = resp.filter((v) => v <= 1).length; return { total, done: done.length, completion: total ? Math.round((done.length / total) * 100) : 0, onTime, onTimeRate: done.length ? Math.round((onTime / done.length) * 100) : 0, late: done.length - onTime, overdue: overdue.length, overdueRate: total ? Math.round((overdue.length / total) * 100) : 0, respAvg: resp.length ? Math.round((resp.reduce((s, v) => s + v, 0) / resp.length) * 10) / 10 : null, respMax: resp.length ? Math.max(...resp) : null, respFast: resp.length ? Math.round((fast / resp.length) * 100) : 0, respCount: resp.length, }; }, [scoped]); const panelHead = (label, title, right) => (
{label}

{title}

{right && {right}}
); const segmented = ( {[["all", "Semua"], ["project", "Project"]].map(([id, label]) => ( ))} ); return (
{/* ---------- baris KPI kinerja: 4 shape ---------- */}
{[ { key: "completion", label: "Task Completion Rate", icon: "circle-check", value: kpi.completion, unit: "%", sub: `${kpi.done} selesai dari ${kpi.total} ditugaskan`, ring: kpi.completion, tone: kpi.completion >= 80 ? "leaf" : kpi.completion >= 50 ? "teal" : "gold" }, { key: "ontime", label: "On-Time Completion", icon: "clock", value: kpi.onTimeRate, unit: "%", sub: kpi.done ? `${kpi.onTime} tepat waktu · ${kpi.late} lewat deadline` : "Belum ada task selesai", ring: kpi.onTimeRate, tone: kpi.onTimeRate >= 80 ? "leaf" : kpi.onTimeRate >= 60 ? "gold" : "coral" }, { key: "overdue", label: "Overdue Task", icon: "triangle-alert", value: kpi.overdue, unit: "", sub: `${kpi.overdueRate}% dari total task periode ini`, ring: kpi.overdueRate, tone: kpi.overdue === 0 ? "leaf" : kpi.overdueRate >= 25 ? "coral" : "gold" }, { key: "resp", label: "Response Time", icon: "timer", value: kpi.respAvg == null ? "—" : kpi.respAvg, unit: kpi.respAvg == null ? "" : " hari", sub: kpi.respAvg == null ? "Belum ada task yang dimulai" : `${kpi.respFast}% mulai ≤1 hari · terlama ${kpi.respMax} hari`, ring: kpi.respFast, tone: kpi.respAvg == null ? "neutral" : kpi.respAvg <= 1 ? "leaf" : kpi.respAvg <= 3 ? "teal" : "coral" }, ].map((m) => { const c = { leaf: "var(--leaf-500)", teal: "var(--teal-500)", gold: "var(--gold-500)", coral: "var(--coral-500)", neutral: "var(--neutral-300)" }[m.tone]; const deep = { leaf: "var(--leaf-600)", teal: "var(--teal-700)", gold: "var(--gold-600)", coral: "var(--coral-600)", neutral: "var(--text-muted)" }[m.tone]; return (
{/* donut ring: satu conic-gradient, tanpa SVG/animasi — ringan */} {m.label} {m.value}{m.unit} {m.sub}
); })}
{/* ---------- baris atas: grafik + KPI ---------- */}
{panelHead("Beban Kerja", "Team Task Tracking", <>{segmented})}
Hi Direktur Klinik Perlu tindakan? {[["shield-check", `${verifying.length} menunggu verifikasi`, "verify"], ["rotate-ccw", `${scoped.filter((t) => t.status === "revision").length} perlu revisi`, "tasks"], ["chart-pie", "Laporan bulan ini", "report"]].map(([icon, label, page]) => ( ))}
onNavigate && onNavigate("tasks")} /> t.status === "in_progress").length} icon="loader" tone="mint" delta="+6%" onOpen={() => onNavigate && onNavigate("tasks")} /> onNavigate && onNavigate("verify")} /> onNavigate && onNavigate("verify")} />
t.status === "in_progress").length} berjalan`} toLabel="Verifikasi admin" toMeta={`${verifying.length} antre`} progress={scoped.length ? Math.round(((accepted + verifying.length) / scoped.length) * 100) : 0} />
{/* ---------- baris kedua: empat panel sejajar, tinggi mengikuti baris ---------- */}
{panelHead("Sebaran kategori", "Kategori task", onNavigate && onNavigate("audit")} />)}
{lensed.length} task
{panelHead("Ritme target", "Waktu pengerjaan", onNavigate && onNavigate("tasks")} />)}
+{accepted} accepted
{panelHead("Periode terpilih", "Peringkat Karyawan", onNavigate && onNavigate("report")} />)}
{ranking.slice(0, 5).map((r, i) => (
{i + 1} {i === 0 && ( )} {r.name} {r.done}/{r.total} selesai · {r.onTimePct}% tepat waktu · skor {r.score} {r.score}%
))}
{panelHead("Task aktif per user", "Beban Kerja Tim", onNavigate && onNavigate("tasks")} />)}
{load.map((w) => (
{w.name} {WORKLOAD_TEXT[w.level]} {w.load}/{w.cap}{w.late ? " · " + w.late + " lewat" : ""}
))} {load.some((w) => w.level === "over") && ( {load.filter((w) => w.level === "over").map((w) => w.name).join(", ")} kelebihan beban — tunda penambahan task atau delegasikan. )}
{/* ---------- baris ketiga: tren periode + target per user ---------- */}
{panelHead("Perbandingan periode", "Tren vs Periode Lalu", trend ? {trend.prevFrom} → {trend.prevTo} : null)} {!trend ? (
Pilih rentang tanggal untuk melihat tren.
) : (
{[ { label: "Task masuk", now: trend.now.masuk, prev: trend.prev.masuk, good: "up" }, { label: "Diterima", now: trend.now.accepted, prev: trend.prev.accepted, good: "up" }, { label: "Terlambat", now: trend.now.late, prev: trend.prev.late, good: "down" }, { label: "Rata-rata progress", now: trend.now.avg, prev: trend.prev.avg, unit: "%", good: "up" }, ].map((m) => { const diff = m.now - m.prev; const better = diff === 0 ? null : m.good === "up" ? diff > 0 : diff < 0; const tone = better == null ? "var(--text-muted)" : better ? "var(--leaf-600)" : "var(--coral-600)"; return (
{m.label} {m.now}{m.unit || ""} 0 ? "trending-up" : "trending-down"} size={13} color={tone} /> {diff === 0 ? "sama" : `${diff > 0 ? "+" : ""}${diff}${m.unit || ""}`} periode lalu {m.prev}{m.unit || ""}
); })}
)}
{panelHead("Capaian target", "Target per User", onNavigate && onNavigate("report")} />)}
{targets.length === 0 ? Belum ada task pada periode ini. : targets.map((t) => (
{t.name} {t.done}/{t.total} · {t.pct}% = 80 ? "var(--leaf-500)" : t.pct >= 50 ? "var(--teal-500)" : "var(--gold-500)"} striped={false} /> {t.late > 0 && {t.late} lewat}
))}
{/* ---------- task table ---------- */}

Semua Task

t.status === "todo").length }, { id: "in_progress", label: "In Progress", count: scoped.filter((t) => t.status === "in_progress").length }, { id: "on_verification", label: "On Verification", count: scoped.filter((t) => t.status === "on_verification").length }, { id: "revision", label: "Need Revision", count: scoped.filter((t) => t.status === "revision").length }, { id: "done", label: "Done", count: scoped.filter((t) => t.status === "done").length }, ]} />
{ const u = userById(t.user); return { key: t.id, type: ( {t.type} {t.repeat && t.repeat !== "none" && {REPEAT_LABEL[t.repeat]}} {t.signature ? : t.needSign ? (t.status === "done" && onSignTask ? : Perlu TTD) : null} ), user: {u.name}, assigned: {t.assigned}, target: {t.target}, progress: {t.progress}%, status: , }; })} /> {filtered.length > pageRows.length && (
Menampilkan {pageRows.length} dari {filtered.length} task
)}
); } Object.assign(window, { AdminDashboard }); })();