(() => {
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 });
})();