TKK_E32230220/public/js/dashboard.js

478 lines
16 KiB
JavaScript

document.addEventListener("DOMContentLoaded", () => {
const $ = (id) => document.getElementById(id);
const isDash = !!$("val_raw_banana");
const isHist = !!$("historyTableBody");
const isFoto = !!$("fotoGrid");
let last = null,
active = false,
timer = null,
startAt = null;
let hf = { mode: "session", sessionId: "", dateFrom: "", dateTo: "" };
let ff = { mode: "session", sessionId: "", dateFrom: "", dateTo: "" };
const csrf = document.querySelector('meta[name="csrf-token"]')?.content;
const e = {
rb: $("val_raw_banana"),
rpb: $("val_ripe_banana"),
ro: $("val_raw_orange"),
rpo: $("val_ripe_orange"),
tbody: $("historyTableBody"),
btn: $("btnSorting"),
ico: $("btnSortIcon"),
lbl: $("btnSortLabel"),
dot: $("indicatorDot"),
txt: $("indicatorText"),
clock: $("sessionTimer"),
refresh: $("btnRefresh"),
};
const fs = $("filterSessionId"),
fr = $("customDateRange"),
fd = $("filterDateFrom"),
ft = $("filterDateTo"),
fa = $("btnApplyFilter");
const ffs = $("filterFotoSessionId"),
ffr = $("customFotoDateRange"),
ffd = $("filterFotoDateFrom"),
fft = $("filterFotoDateTo"),
ffa = $("btnApplyFotoFilter"),
fl = $("filterFotoLabel"),
rf = $("btnRefreshFoto");
const grid = $("fotoGrid"),
modal = $("fotoModal"),
mimg = $("fotoModalImg"),
mlbl = $("fotoModalLabel"),
mtime = $("fotoModalTime"),
mclose = $("fotoModalClose");
const cm = $("confirmModal"),
mc = $("modalBtnConfirm"),
mx = $("modalBtnCancel"),
toast = $("toastContainer");
function ui(a, s = null) {
if (!e.btn) return;
active = a;
e.btn.disabled = false;
if (a) {
e.btn.className = "btn-sorting btn-stop";
e.ico.innerHTML = '<i class="fas fa-stop"></i>';
e.lbl.textContent = "Stop Sortir";
e.dot.className = "indicator-dot active";
e.txt.className = "indicator-text active";
e.txt.textContent = "Sortir Berjalan";
if (s) {
startAt = new Date(s);
runTimer();
}
} else {
e.btn.className = "btn-sorting btn-start";
e.ico.innerHTML = '<i class="fas fa-play"></i>';
e.lbl.textContent = "Mulai Sortir";
e.dot.className = "indicator-dot idle";
e.txt.className = "indicator-text";
e.txt.textContent = "Siap";
e.clock.textContent = "";
startAt = null;
clearInterval(timer);
}
}
function runTimer() {
clearInterval(timer);
timer = setInterval(() => {
if (!startAt || !e.clock) return;
let d = Math.floor((new Date() - startAt) / 1000),
h = String(Math.floor(d / 3600)).padStart(2, "0"),
m = String(Math.floor((d % 3600) / 60)).padStart(2, "0"),
s = String(d % 60).padStart(2, "0");
e.clock.textContent = "⏱ " + h + ":" + m + ":" + s;
}, 1000);
}
async function status() {
try {
let r = await fetch("/api/session/status"),
d = await r.json();
ui(d.active, d.session?.started_at);
} catch {
ui(false);
}
}
function note(t, m, type = "info") {
if (!toast) return;
let n = document.createElement("div");
n.className = "toast toast-" + type;
n.innerHTML =
'<div class="toast-icon"><i class="fas fa-info-circle"></i></div><div class="toast-body"><div class="toast-title">' +
t +
'</div><div class="toast-message">' +
m +
'</div></div><button class="toast-close"><i class="fas fa-times"></i></button>';
n.querySelector("button").onclick = () => n.remove();
toast.appendChild(n);
setTimeout(() => n.remove(), 4000);
}
function ask() {
return new Promise((ok) => {
if (!cm) return ok(true);
cm.classList.add("show");
mc.onclick = () => {
cm.classList.remove("show");
ok(true);
};
mx.onclick = () => {
cm.classList.remove("show");
ok(false);
};
});
}
async function start() {
if (!e.btn) return;
e.btn.disabled = true;
try {
let r = await fetch("/api/session/start", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrf,
},
}),
d = await r.json();
d.success
? (ui(true, d.session.started_at),
load(),
note("Sortir Dimulai", "Sesi dimulai", "success"))
: (note("Gagal", d.message || "Gagal", "error"), ui(false));
} catch {
note("Error", "Server gagal", "error");
ui(false);
}
}
async function stop() {
if (!(await ask())) return;
if (!e.btn) return;
e.btn.disabled = true;
try {
let r = await fetch("/api/session/stop", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrf,
},
}),
d = await r.json();
d.success
? (ui(false),
load(),
note("Sortir Selesai", "Data dikalkulasi", "success"))
: (note("Gagal", d.message || "Gagal", "error"), status());
} catch {
note("Error", "Server gagal", "error");
status();
}
}
if (e.btn) e.btn.onclick = () => (active ? stop() : start());
function tabs(q, state, sel, custom, cb) {
document.querySelectorAll(q).forEach(
(b) =>
(b.onclick = () => {
document
.querySelectorAll(q)
.forEach((x) => x.classList.remove("active"));
b.classList.add("active");
state.mode = b.dataset.filter;
if (sel)
sel.style.display =
state.mode === "session" ? "" : "none";
if (custom)
custom.style.display =
state.mode === "custom" ? "flex" : "none";
cb();
}),
);
}
if (isHist || isDash) {
tabs(
"#tabSession,#tabToday,#tabWeek,#tabMonth,#tabCustom",
hf,
fs,
fr,
load,
);
if (fs) {
fs.style.display = "";
fs.onchange = () => {
hf.sessionId = fs.value;
load();
};
}
if (fa) {
fa.onclick = () => {
hf.dateFrom = fd.value;
hf.dateTo = ft.value;
load();
};
}
}
if (isFoto || isDash) {
tabs(
"#tabFotoSession,#tabFotoToday,#tabFotoWeek,#tabFotoMonth,#tabFotoCustom",
ff,
ffs,
ffr,
loadFoto,
);
if (ffs) {
ffs.style.display = "";
ffs.onchange = () => {
ff.sessionId = ffs.value;
loadFoto();
};
}
if (ffa) {
ffa.onclick = () => {
ff.dateFrom = ffd.value;
ff.dateTo = fft.value;
loadFoto();
};
}
}
function qs(st) {
let p = new URLSearchParams({ filter: st.mode });
if (st.mode === "session" && st.sessionId)
p.set("session_id", st.sessionId);
if (st.mode === "custom") {
if (st.dateFrom) p.set("date_from", st.dateFrom);
if (st.dateTo) p.set("date_to", st.dateTo);
}
return p;
}
function fillSessions(s = []) {
[fs, ffs].forEach((sel) => {
if (!sel) return;
let v = sel.value;
sel.innerHTML = '<option value="">Sesi Aktif / Terakhir</option>';
s.forEach((x) => {
let o = document.createElement("option");
o.value = x.id;
o.textContent =
new Date(x.started_at).toLocaleString("id-ID") +
" (" +
(x.total_records || 0) +
" buah)";
sel.appendChild(o);
});
sel.value = v;
});
}
async function load() {
try {
let r = await fetch("/api/latest-data?" + qs(hf)),
d = await r.json();
if (d.sessions) fillSessions(d.sessions);
draw(d);
} catch (err) {
console.error(err);
}
}
function anim(o, a, b) {
if (!o) return;
let st = null;
requestAnimationFrame(function f(t) {
if (!st) st = t;
let p = Math.min((t - st) / 800, 1);
o.textContent = (a + (b - a) * p).toFixed(2);
if (p < 1) requestAnimationFrame(f);
});
}
function draw(d) {
// Update Ringkasan Kalkulasi Sesi secara dinamis
if (d.totals) {
const getVal = (v) => (v !== undefined && v !== null) ? Number(v).toFixed(2) : "0.00";
const getCount = (v) => (v !== undefined && v !== null) ? Number(v) : "0";
const sumRawBanana = $("sum_raw_banana");
const sumRipeBanana = $("sum_ripe_banana");
const sumRawOrange = $("sum_raw_orange");
const sumRipeOrange = $("sum_ripe_orange");
const sumTotalRecords = $("sum_total_records");
const sumGrandTotal = $("sum_grand_total");
if (sumRawBanana) sumRawBanana.textContent = getVal(d.totals.total_raw_banana_weight);
if (sumRipeBanana) sumRipeBanana.textContent = getVal(d.totals.total_ripe_banana_weight);
if (sumRawOrange) sumRawOrange.textContent = getVal(d.totals.total_raw_orange_weight);
if (sumRipeOrange) sumRipeOrange.textContent = getVal(d.totals.total_ripe_orange_weight);
if (sumTotalRecords) sumTotalRecords.textContent = getCount(d.totals.total_records);
if (sumGrandTotal) {
const grandTotal = Number(d.totals.total_raw_banana_weight || 0) +
Number(d.totals.total_ripe_banana_weight || 0) +
Number(d.totals.total_raw_orange_weight || 0) +
Number(d.totals.total_ripe_orange_weight || 0);
sumGrandTotal.textContent = grandTotal.toFixed(2);
}
}
if (!d.latest) {
if (isHist || isDash)
e.tbody.innerHTML =
'<tr><td colspan="5" class="empty-state">Belum ada riwayat klasifikasi.</td></tr>';
if (isDash)
[e.rb, e.rpb, e.ro, e.rpo].forEach(
(x) => (x.textContent = "0.00"),
);
return;
}
let id = d.latest.id || d.latest._id;
if (id !== last) {
last = id;
if (isDash) {
anim(
e.rb,
+e.rb.textContent || 0,
+d.latest.raw_banana_weight || 0,
);
anim(
e.rpb,
+e.rpb.textContent || 0,
+d.latest.ripe_banana_weight || 0,
);
anim(
e.ro,
+e.ro.textContent || 0,
+d.latest.raw_orange_weight || 0,
);
anim(
e.rpo,
+e.rpo.textContent || 0,
+d.latest.ripe_orange_weight || 0,
);
setTimeout(
() =>
[e.rb, e.rpb, e.ro, e.rpo].forEach((x) =>
anim(x, +x.textContent || 0, 0),
),
4000,
);
}
}
if (isHist || isDash) {
e.tbody.innerHTML = "";
(d.history || []).forEach((i) => {
let tr = document.createElement("tr");
tr.innerHTML =
"<td>" +
new Date(i.classification_date).toLocaleString("id-ID") +
"</td><td>" +
i.raw_banana_weight +
"</td><td>" +
i.ripe_banana_weight +
"</td><td>" +
i.raw_orange_weight +
"</td><td>" +
i.ripe_orange_weight +
"</td>";
e.tbody.appendChild(tr);
});
if (!(d.history || []).length)
e.tbody.innerHTML =
'<tr><td colspan="5" class="empty-state">Belum ada riwayat klasifikasi.</td></tr>';
}
}
async function loadFoto() {
if (!grid) return;
try {
let p = qs(ff);
if (fl && fl.value) p.set("label", fl.value);
let r = await fetch("/api/foto?" + p),
d = await r.json();
if (!d.fotos?.length) {
grid.innerHTML =
'<div class="foto-empty">Belum ada foto tersimpan.</div>';
return;
}
grid.innerHTML = d.fotos
.map(
(f) =>
'<div class="foto-card" data-b="' +
f.image_base64 +
'" data-l="' +
f.label +
'" data-t="' +
f.captured_at +
'"><img src="data:image/jpeg;base64,' +
f.image_base64 +
'"><div class="foto-card-info"><span class="foto-card-label ' +
(f.label.includes("matang") ? "matang" : "mentah") +
'">' +
f.label +
'</span><span class="foto-card-time">' +
new Date(f.captured_at).toLocaleString("id-ID") +
"</span></div></div>",
)
.join("");
if (modal) {
grid.querySelectorAll(".foto-card").forEach(
(c) =>
(c.onclick = () => {
if (mimg)
mimg.src =
"data:image/jpeg;base64," + c.dataset.b;
if (mlbl) mlbl.textContent = c.dataset.l;
if (mtime)
mtime.textContent = new Date(
c.dataset.t,
).toLocaleString("id-ID");
modal.classList.add("show");
}),
);
}
} catch {
grid.innerHTML = '<div class="foto-empty">Gagal memuat foto.</div>';
}
}
if (e.refresh) e.refresh.onclick = load;
if (rf) rf.onclick = loadFoto;
if (fl) fl.onchange = loadFoto;
if (mclose) mclose.onclick = () => modal.classList.remove("show");
if (modal)
modal.onclick = (x) => {
if (x.target === modal) modal.classList.remove("show");
};
status();
if (isDash || isHist) {
(async function p() {
await load();
setTimeout(p, 1000);
})();
}
if (isDash || isFoto) {
(async function pf() {
await loadFoto();
setTimeout(pf, 5000);
})();
}
});