456 lines
21 KiB
HTML
456 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||
<meta name="description" content="">
|
||
<meta name="author" content="">
|
||
|
||
<title>SB Admin 2 - Tables</title>
|
||
|
||
<!-- Custom fonts for this template -->
|
||
<link href="vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
|
||
<link
|
||
href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i"
|
||
rel="stylesheet">
|
||
|
||
<!-- Custom styles for this template -->
|
||
<link href="css/sb-admin-2.min.css" rel="stylesheet">
|
||
<link href="css/custom.css" rel="stylesheet">
|
||
|
||
<!-- Custom styles for this page -->
|
||
<link href="vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||
<style>
|
||
/* Hide DataTables search box and columns text */
|
||
.dataTables_filter { display: none !important; }
|
||
/* Style DataTables bottom controls to match tables.html */
|
||
.dataTables_length { margin-right: 1.5rem; }
|
||
.dataTables_length label { margin-bottom: 0; display: flex; align-items: center; gap: 8px; font-size: 0.875rem; color: #6c757d; }
|
||
.dataTables_length select { width: auto !important; display: inline-block !important; }
|
||
.dataTables_info { font-size: 0.875rem; color: #6c757d; padding-top: 0 !important; }
|
||
</style>
|
||
|
||
</head>
|
||
|
||
<body id="page-top">
|
||
|
||
<!-- Page Wrapper -->
|
||
<div id="wrapper">
|
||
|
||
<!-- Content Wrapper -->
|
||
<div id="content-wrapper" class="d-flex flex-column">
|
||
|
||
<!-- Main Content -->
|
||
<div id="content">
|
||
|
||
<!-- Topbar -->
|
||
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
|
||
|
||
<!-- Page Title - Centered -->
|
||
<div class="d-flex align-items-center justify-content-center w-100">
|
||
<h1 class="h3 mb-0 text-gray-800 dashboard-title">Positif</h1>
|
||
</div>
|
||
|
||
<!-- Topbar Navbar -->
|
||
<ul class="navbar-nav ml-auto">
|
||
</ul>
|
||
|
||
</nav>
|
||
<!-- End of Topbar -->
|
||
|
||
<!-- Begin Page Content -->
|
||
<div class="container-fluid">
|
||
|
||
<!-- Page Heading -->
|
||
<div class="d-flex align-items-center justify-content-between mb-4" style="position: relative;">
|
||
<a href="index.html" class="btn btn-secondary shadow-sm" style="display: inline-flex; align-items: center; gap: 8px;">
|
||
<i class="fas fa-arrow-left fa-sm"></i> Kembali
|
||
</a>
|
||
</div>
|
||
|
||
<!-- DataTales Example -->
|
||
<div class="card shadow mb-4">
|
||
<div class="card-header py-3">
|
||
<h6 class="m-0 font-weight-bold text-primary">Uploaded Data (Positif)</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
|
||
<thead>
|
||
<tr>
|
||
<th> </th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Belum ada data yang di-upload. Silakan upload file di halaman Dashboard.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="mt-3">
|
||
<small id="uploadedFileName" class="text-muted d-block"> </small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<!-- /.container-fluid -->
|
||
|
||
</div>
|
||
<!-- End of Main Content -->
|
||
|
||
<!-- Footer -->
|
||
<footer class="sticky-footer bg-white">
|
||
<div class="container my-auto">
|
||
<div class="copyright text-center my-auto">
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
<!-- End of Footer -->
|
||
|
||
</div>
|
||
<!-- End of Content Wrapper -->
|
||
|
||
</div>
|
||
<!-- End of Page Wrapper -->
|
||
|
||
<!-- Scroll to Top Button-->
|
||
<a class="scroll-to-top rounded" href="#page-top">
|
||
<i class="fas fa-angle-up"></i>
|
||
</a>
|
||
|
||
<!-- Logout Modal-->
|
||
<div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel"
|
||
aria-hidden="true">
|
||
<div class="modal-dialog" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="exampleModalLabel">Ready to Leave?</h5>
|
||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||
<a class="btn btn-primary" href="login.html">Logout</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bootstrap core JavaScript-->
|
||
<script src="vendor/jquery/jquery.min.js"></script>
|
||
<script src="vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||
|
||
<!-- Core plugin JavaScript-->
|
||
<script src="vendor/jquery-easing/jquery.easing.min.js"></script>
|
||
|
||
<!-- Custom scripts for all pages-->
|
||
<script src="js/sb-admin-2.min.js"></script>
|
||
|
||
<!-- Page level plugins -->
|
||
<script src="vendor/datatables/jquery.dataTables.min.js"></script>
|
||
<script src="vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||
<script>
|
||
// Apply pending topbar search when this page loads
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
try {
|
||
const search = sessionStorage.getItem('topbar_search');
|
||
const target = sessionStorage.getItem('topbar_search_target');
|
||
if (search && target === 'tables') {
|
||
const input = document.querySelector('#tableSearchInput');
|
||
if (input) { input.value = search; const ev = new Event('input', { bubbles: true }); input.dispatchEvent(ev); }
|
||
// clear the pending search so it doesn't reapply on reload
|
||
sessionStorage.removeItem('topbar_search');
|
||
sessionStorage.removeItem('topbar_search_target');
|
||
}
|
||
} catch (e) { /* ignore storage errors */ }
|
||
});
|
||
</script>
|
||
|
||
<!-- Page level custom scripts -->
|
||
<!-- <script src="js/demo/datatables-demo.js"></script> -->
|
||
|
||
<!-- Load uploaded data from sessionStorage and populate the DataTable with POSITIVE rows only -->
|
||
<script>
|
||
// Fungsi untuk melarikan (escape) karakter HTML agar aman ditampilkan
|
||
function escapeHtml(text) {
|
||
return String(text === null || text === undefined ? '' : text)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
// Fungsi untuk mendeteksi indeks kolom sentimen positif berdasarkan header dan baris data
|
||
function detectPositiveColumnIndex(header, rows) {
|
||
// Mencoba mendeteksi berdasarkan nama header, tetapi diverifikasi dengan memeriksa nilai kolom (heuristik berbasis nilai)
|
||
if (!header || !header.length) return -1;
|
||
const headersLower = header.map(h => String(h || '').toLowerCase());
|
||
|
||
// Fungsi internal untuk mengecek apakah suatu nilai adalah label sentimen
|
||
function isLabelValue(v) {
|
||
if (v === null || v === undefined) return false;
|
||
const s = String(v).trim().toLowerCase();
|
||
if (!s) return false;
|
||
if (/^(positif|positive|pos|p|1|true|negatif|negative|neg|n|0|false)$/i.test(s)) return true;
|
||
if (s.includes('positif') || s.includes('positive') || s.includes('negatif') || s.includes('negative')) return true;
|
||
return false;
|
||
}
|
||
|
||
// Periksa kandidat header terlebih dahulu dan hitung tingkat kecocokan
|
||
const headerCandidates = [];
|
||
for (let i = 0; i < headersLower.length; i++) {
|
||
if (/\b(sentim|sentimen|sentiment|label|kelas|predik|hasil|pelabel|pelabelan)\b/i.test(headersLower[i])) headerCandidates.push(i);
|
||
}
|
||
// Fungsi internal untuk menghitung tingkat kecocokan label pada kolom
|
||
function columnLabelMatchRate(idx) {
|
||
let match = 0, total = 0;
|
||
for (const r of rows) {
|
||
const v = r[idx];
|
||
if (v !== null && v !== undefined && String(v).trim() !== '') {
|
||
total++; if (isLabelValue(v)) match++; }
|
||
}
|
||
return total === 0 ? 0 : match / total;
|
||
}
|
||
|
||
let bestIdx = -1, bestRate = 0;
|
||
for (const idx of headerCandidates) {
|
||
const rate = columnLabelMatchRate(idx);
|
||
if (rate > bestRate) { bestRate = rate; bestIdx = idx; }
|
||
}
|
||
// memerlukan tingkat kecocokan yang lebih ketat untuk menerima kandidat header (mengurangi positif palsu)
|
||
if (bestRate >= 0.25) return bestIdx;
|
||
|
||
// Cadangan: pindai semua kolom dan pilih satu dengan tingkat kecocokan label tertinggi
|
||
for (let i = 0; i < header.length; i++) {
|
||
const rate = columnLabelMatchRate(i);
|
||
if (rate > bestRate) { bestRate = rate; bestIdx = i; }
|
||
}
|
||
if (bestRate >= 0.25) return bestIdx;
|
||
|
||
// Sebagai cadangan terakhir, gunakan heuristik sebelumnya yang mencari banyak nilai seperti positif
|
||
const scores = new Array(header.length).fill(0);
|
||
function isPos(v) {
|
||
if (v === null || v === undefined) return false;
|
||
const s = String(v).toLowerCase();
|
||
return /^(positif|positive|pos|p|1|true)$/i.test(s) || s.includes('positif') || s.includes('positive');
|
||
}
|
||
for (const r of rows) {
|
||
for (let i = 0; i < header.length; i++) if (isPos(r[i])) scores[i]++;
|
||
}
|
||
let best = -1, bestScore = 0;
|
||
for (let i = 0; i < scores.length; i++) { if (scores[i] > bestScore) { best = i; bestScore = scores[i]; } }
|
||
if (bestScore > 0) return best;
|
||
return -1; // tidak ditemukan
|
||
}
|
||
|
||
// Fungsi untuk mengecek apakah suatu nilai menunjukkan sentimen positif
|
||
function isPositiveValue(v) {
|
||
if (v === null || v === undefined) return false;
|
||
const s = String(v).trim().toLowerCase();
|
||
// cocokkan kata utuh atau indikator numerik; hindari kecocokan substring di dalam teks panjang
|
||
return /\b(positif|positive|pos|p)\b/i.test(s) || /^(1|true)$/i.test(s);
|
||
}
|
||
|
||
// Penilaian sentimen heuristik berdasarkan daftar kata kunci (Bahasa Indonesia + Inggris)
|
||
function sentimentScoreRow(row, header) {
|
||
const candidates = ['text','teks','isi','tweet','message','review','komentar','comment','isi_pesan','judul'];
|
||
let text = '';
|
||
for (let i = 0; i < header.length; i++) {
|
||
const h = String(header[i] || '').toLowerCase();
|
||
if (candidates.some(c => h.includes(c))) { text = String(row[i] || ''); break; }
|
||
}
|
||
if (!text) text = (row || []).join(' ');
|
||
const s = String(text).toLowerCase();
|
||
|
||
const positiveWords = ['bagus','baik','senang','suka','puas','terbaik','mantap','bagus sekali','cinta','positif','recommend','love','great','excellent','amazing','happy'];
|
||
const negativeWords = ['buruk','jelek','sedih','benci','tidak suka','gagal','buruk sekali','hate','bad','terrible','poor','sad','angry'];
|
||
let score = 0;
|
||
for (const w of positiveWords) { if (s.indexOf(w) !== -1) score++; }
|
||
for (const w of negativeWords) { if (s.indexOf(w) !== -1) score--; }
|
||
return score;
|
||
}
|
||
|
||
// Fungsi untuk mengecek apakah baris data memiliki sentimen positif berdasarkan skor heuristik
|
||
function isPositiveBySentiment(row, header) {
|
||
return sentimentScoreRow(row, header) > 0;
|
||
}
|
||
|
||
// Fungsi utama untuk merender tabel data positif
|
||
function renderPositifDataTable() {
|
||
try {
|
||
const raw = sessionStorage.getItem('uploadedTableData');
|
||
const fileEl = document.getElementById('uploadedFileName');
|
||
const countEl = document.getElementById('positifCount');
|
||
if (!raw) {
|
||
if (fileEl) fileEl.textContent = '';
|
||
if (countEl) countEl.textContent = '';
|
||
if ($.fn.DataTable.isDataTable('#dataTable')) {
|
||
try { $('#dataTable').DataTable().clear().destroy(); } catch (e) { }
|
||
}
|
||
const thead = document.querySelector('#dataTable thead');
|
||
const tbody = document.querySelector('#dataTable tbody');
|
||
thead.innerHTML = '<tr><th> </th></tr>';
|
||
tbody.innerHTML = '<tr><td>Belum ada data yang di-upload. Silakan upload file di halaman Dashboard.</td></tr>';
|
||
|
||
// Inisialisasi DataTable kosong tanpa fitur pencarian
|
||
if (!$.fn.DataTable.isDataTable('#dataTable')) {
|
||
$('#dataTable').DataTable({
|
||
searching: false,
|
||
ordering: false,
|
||
info: false,
|
||
paging: false,
|
||
language: {
|
||
zeroRecords: 'Belum ada data yang di-upload. Silakan upload file di halaman Dashboard.'
|
||
}
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
const payload = JSON.parse(raw);
|
||
if (!payload || !payload.header || !payload.rows) return false;
|
||
|
||
const header = payload.header;
|
||
const rows = payload.rows;
|
||
if (fileEl) fileEl.textContent = payload.fileName || '';
|
||
|
||
// deteksi kolom positif (disimpan untuk referensi) tetapi pemilihan dilakukan oleh heuristik sentimen
|
||
const posIdx = detectPositiveColumnIndex(header, rows);
|
||
const thead = document.querySelector('#dataTable thead');
|
||
const tfoot = document.querySelector('#dataTable tfoot');
|
||
const tbody = document.querySelector('#dataTable tbody');
|
||
|
||
// membangun header dan footer tabel
|
||
const headRow = document.createElement('tr');
|
||
const footRow = document.createElement('tr');
|
||
for (const h of header) {
|
||
const th = document.createElement('th'); th.textContent = String(h);
|
||
headRow.appendChild(th);
|
||
const thf = document.createElement('th'); thf.textContent = String(h);
|
||
footRow.appendChild(thf);
|
||
}
|
||
thead.innerHTML = ''; thead.appendChild(headRow);
|
||
if (tfoot) { tfoot.innerHTML = ''; tfoot.appendChild(footRow); }
|
||
|
||
// memfilter hanya baris data positif
|
||
const positiveRows = [];
|
||
|
||
if (posIdx === -1) {
|
||
console.error('Kolom Sentiment tidak ditemukan');
|
||
} else {
|
||
for (const r of rows) {
|
||
const val = String(r[posIdx] || '').trim().toLowerCase();
|
||
if (val === 'positif') {
|
||
positiveRows.push(r);
|
||
}
|
||
}
|
||
}
|
||
|
||
// memperbarui jumlah data positif di UI
|
||
if (countEl) {
|
||
countEl.textContent = 'Positif: ' + positiveRows.length + ' / ' + rows.length;
|
||
}
|
||
|
||
// membangun isi (body) tabel
|
||
let bodyHtml = '';
|
||
if (positiveRows.length === 0) {
|
||
bodyHtml = '<tr><td>Tidak ada baris dengan sentimen positif yang terdeteksi.</td></tr>';
|
||
} else {
|
||
for (const r of positiveRows) {
|
||
bodyHtml += '<tr>';
|
||
for (let i = 0; i < header.length; i++) {
|
||
const cell = i < r.length ? escapeHtml(r[i]) : '';
|
||
// jika ini adalah kolom sentimen/label yang terdeteksi, render sebagai badge
|
||
if (i === posIdx) {
|
||
const rawVal = i < r.length ? String(r[i]) : '';
|
||
const isPosVal = isPositiveValue(rawVal);
|
||
const badgeClass = isPosVal ? 'pos' : 'neg';
|
||
bodyHtml += '<td><span class="sent-badge ' + badgeClass + '">' + escapeHtml(rawVal) + '</span></td>';
|
||
} else {
|
||
bodyHtml += '<td>' + cell + '</td>';
|
||
}
|
||
}
|
||
bodyHtml += '</tr>';
|
||
}
|
||
}
|
||
tbody.innerHTML = bodyHtml;
|
||
|
||
// menginisialisasi ulang DataTables dengan aman dan menerapkan label yang sama
|
||
if ($.fn.DataTable.isDataTable('#dataTable')) {
|
||
try { $('#dataTable').DataTable().clear().destroy(); } catch (e) { /* abaikan */ }
|
||
}
|
||
$('#dataTable').DataTable({
|
||
dom: 'rt<"d-flex align-items-center justify-content-between mt-2" <"d-flex align-items-center"li> p>',
|
||
ordering: false,
|
||
pageLength: 10,
|
||
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
|
||
pagingType: 'simple_numbers',
|
||
searching: false,
|
||
language: {
|
||
lengthMenu: 'Tampilkan _MENU_',
|
||
info: 'Menampilkan _START_–_END_ dari _TOTAL_',
|
||
infoEmpty: 'Menampilkan 0–0 dari 0',
|
||
zeroRecords: 'Tidak ada data',
|
||
paginate: { previous: 'Previous', next: 'Next' },
|
||
search: 'Cari:'
|
||
}
|
||
});
|
||
return true;
|
||
} catch (e) {
|
||
console.warn('renderPositifDataTable error', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Fungsi pembantu: melakukan pencarian pada DataTable atau cadangan
|
||
function performTableSearch(q) {
|
||
try {
|
||
if ($.fn.DataTable.isDataTable('#dataTable')) {
|
||
const dt = $('#dataTable').DataTable(); dt.search(q).draw();
|
||
} else {
|
||
// cadangan: filter DOM sederhana
|
||
const tbody = document.querySelector('#dataTable tbody');
|
||
if (!tbody) return;
|
||
const rows = tbody.querySelectorAll('tr');
|
||
const qq = String(q).toLowerCase();
|
||
rows.forEach(function (r) {
|
||
const text = (r.textContent || '').toLowerCase(); r.style.display = text.includes(qq) ? '' : 'none';
|
||
});
|
||
}
|
||
} catch (e) { }
|
||
}
|
||
|
||
// run on page load
|
||
document.addEventListener('DOMContentLoaded', function () { try { renderPositifDataTable(); } catch (e){} });
|
||
|
||
// consume topbar search when arriving here (target 'positif')
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
try {
|
||
const q = sessionStorage.getItem('topbar_search');
|
||
const t = sessionStorage.getItem('topbar_search_target');
|
||
if (q && t === 'positif') {
|
||
// small delay to allow DataTable to initialize
|
||
setTimeout(function () { try { performTableSearch(q); } catch (e) {} }, 50);
|
||
sessionStorage.removeItem('topbar_search'); sessionStorage.removeItem('topbar_search_target');
|
||
}
|
||
} catch (e) {}
|
||
});
|
||
|
||
// update when uploaded data changes in other tabs
|
||
window.addEventListener('storage', function (e) {
|
||
if (e.key === 'uploadedTableData' || e.key === 'uploadedTableData_update') {
|
||
renderPositifDataTable();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
|
||
</html> |