Spaces:
Running
Running
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Balance Position Manager - Hidden Kolom via Checklist</title> | |
| <script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script> | |
| <style> | |
| body { font-family: Arial, sans-serif; margin: 0; background-color: #f4f6f9; } | |
| h1 { text-align: center; color: #333; padding: 20px; background: #007bff; color: white; margin: 0; } | |
| .container { max-width: 1600px; margin: 20px auto; padding: 0 20px; display: flex; gap: 20px; } | |
| .sidebar { width: 300px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 20px; height: fit-content; } | |
| .main { flex: 1; } | |
| .tabs { display: flex; margin-bottom: 20px; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } | |
| .tab-btn { flex: 1; padding: 15px; text-align: center; background: #e9ecef; cursor: pointer; font-weight: bold; } | |
| .tab-btn.active { background: #007bff; color: white; } | |
| .tab-content { display: none; } | |
| .tab-content.active { display: block; } | |
| .controls { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } | |
| label { font-weight: bold; } | |
| input, select, button { padding: 10px; font-size: 16px; border-radius: 5px; border: 1px solid #ccc; } | |
| button { background: #007bff; color: white; border: none; cursor: pointer; } | |
| button:hover { background: #0056b3; } | |
| button.warning { background: #ffc107; color: #212529; } | |
| button.warning:hover { background: #e0a800; } | |
| button.danger { background: #dc3545; } | |
| button.danger:hover { background: #c82333; } | |
| #fileList { list-style: none; padding: 0; margin: 0; } | |
| #fileList li { padding: 12px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } | |
| #fileList li:hover { background: #f8f9fa; } | |
| .file-actions button { padding: 6px 12px; font-size: 14px; margin-left: 5px; } | |
| #output { overflow-x: auto; box-shadow: 0 2px 10px rgba(0,0,0,0.1); border-radius: 8px; } | |
| table { border-collapse: collapse; width: 100%; background: white; } | |
| th, td { border: 1px solid #ddd; padding: 10px; text-align: right; font-size: 13px; } | |
| th { background: #007bff; color: white; cursor: pointer; position: sticky; top: 0; z-index: 10; } | |
| th.header, td.header { text-align: left; background: #e9ecef; font-weight: bold; } | |
| th.sorted-asc::after { content: " ↑"; } | |
| th.sorted-desc::after { content: " ↓"; } | |
| tr:nth-child(even) { background: #f8f9fa; } | |
| .no-data { text-align: center; padding: 30px; color: #666; font-style: italic; } | |
| .column-list { max-height: 600px; overflow-y: auto; margin-top: 10px; } | |
| .column-item { display: flex; align-items: center; padding: 8px 0; } | |
| .column-item input { margin-right: 10px; transform: scale(1.2); } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Balance Position Manager</h1> | |
| <div class="container"> | |
| <div class="sidebar" id="sidebar" style="display:none;"> | |
| <h3>Hidden Kolom</h3> | |
| <button class="warning" onclick="toggleHideMode()" style="width:100%; margin-bottom:15px;"> | |
| <span id="hideModeBtnText">Mode Hidden Kolom</span> | |
| </button> | |
| <div id="columnChecklist" class="column-list"></div> | |
| </div> | |
| <div class="main"> | |
| <div class="tabs"> | |
| <div class="tab-btn active" onclick="openTab('upload')">Upload & Proses</div> | |
| <div class="tab-btn" onclick="openTab('saved')">File Tersimpan (<span id="fileCount">0</span>)</div> | |
| </div> | |
| <div id="upload" class="tab-content active"> | |
| <div class="controls"> | |
| <input type="file" id="fileInput" accept=".txt"> | |
| <input type="text" id="fileNameInput" placeholder="Nama file (misal: Nov 2025)" style="width:300px;"> | |
| <button onclick="processAndSave()">Proses & Simpan</button> | |
| </div> | |
| <div class="controls"> | |
| <label for="typeFilter">Filter Type:</label> | |
| <select id="typeFilter" onchange="applyFilters()"> | |
| <option value="">Semua Type</option> | |
| </select> | |
| <label for="searchInput" style="margin-left:20px;">Cari Kode:</label> | |
| <input type="text" id="searchInput" placeholder="Ketik AADI, BBCA, dll" onkeyup="applyFilters()"> | |
| </div> | |
| <div id="output"></div> | |
| </div> | |
| <div id="saved" class="tab-content"> | |
| <h3>File yang Telah Disimpan</h3> | |
| <ul id="fileList"></ul> | |
| <p class="no-data" id="noFilesMsg">Belum ada file yang disimpan.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let allRows = []; | |
| let currentFileKey = null; | |
| let uniqueTypes = new Set(); | |
| let hiddenColumns = new Set(); | |
| let fullHeaders = []; | |
| let hideMode = false; | |
| function openTab(tabName) { | |
| document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById(tabName).classList.add('active'); | |
| document.querySelector(`.tab-btn[onclick="openTab('${tabName}')"]`).classList.add('active'); | |
| if (tabName === 'saved') loadSavedFiles(); | |
| } | |
| async function processAndSave() { | |
| const fileInput = document.getElementById('fileInput'); | |
| const nameInput = document.getElementById('fileNameInput').value.trim(); | |
| const file = fileInput.files[0]; | |
| if (!file) return alert('Pilih file TXT terlebih dahulu!'); | |
| if (!nameInput) return alert('Masukkan nama file!'); | |
| const content = await readFileAsText(file); | |
| const parsed = parseContent(content); | |
| const key = 'balance_' + Date.now(); | |
| const savedData = { | |
| name: nameInput, | |
| date: new Date().toLocaleString('id-ID'), | |
| content: content, | |
| headers: parsed.headers, | |
| rows: parsed.rows, | |
| types: Array.from(parsed.types) | |
| }; | |
| await localforage.setItem(key, savedData); | |
| alert(`File "${nameInput}" berhasil disimpan!`); | |
| document.getElementById('fileNameInput').value = ''; | |
| fileInput.value = ''; | |
| displayTable(savedData); | |
| updateFileCount(); | |
| } | |
| function readFileAsText(file) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = e => resolve(e.target.result); | |
| reader.onerror = reject; | |
| reader.readAsText(file); | |
| }); | |
| } | |
| function parseContent(content) { | |
| const lines = content.trim().split('\n'); | |
| if (lines.length < 2) throw new Error('File kosong atau salah format.'); | |
| const baseHeaders = lines[0].split('|').map(h => h.trim()); | |
| const localPercentNames = ['Local Isur %', 'Local CP %', 'Local PF %', 'Local IB %', 'Local ID %', 'Local MF %', 'Local SC %', 'Local FD %', 'Local OT %']; | |
| const foreignPercentNames = ['Foreign IS %', 'Foreign CP %', 'Foreign PF %', 'Foreign IB %', 'Foreign ID %', 'Foreign MF %', 'Foreign SC %', 'Foreign FD %', 'Foreign OT %']; | |
| const extraHeaders = ['ttotal', ...localPercentNames, 'Total lok %', ...foreignPercentNames]; | |
| const fullHeaders = [...baseHeaders, ...extraHeaders]; | |
| const rows = []; | |
| const types = new Set(); | |
| lines.slice(1).forEach(line => { | |
| const row = line.split('|').map(c => c.trim()); | |
| if (row.length < baseHeaders.length) return; | |
| const code = row[1].toUpperCase(); | |
| const type = row[2]; | |
| types.add(type); | |
| const secNum = parseFloat(row[3]) || 0; | |
| const localValues = row.slice(5, 14).map(v => parseFloat(v) || 0); | |
| const totalLocal = localValues.reduce((a, b) => a + b, 0); | |
| const foreignValues = row.slice(15, 24).map(v => parseFloat(v) || 0); | |
| const totalForeign = foreignValues.reduce((a, b) => a + b, 0); | |
| const ttotal = totalLocal + totalForeign; | |
| const formatPercentValue = (val) => secNum > 0 ? (val / secNum * 100).toFixed(2) : '0.00'; | |
| const displayRow = [ | |
| ...row, | |
| ttotal.toLocaleString(), | |
| ...localValues.map(formatPercentValue), | |
| formatPercentValue(totalLocal), | |
| ...foreignValues.map(formatPercentValue) | |
| ]; | |
| const numericRow = [ | |
| ...row.slice(0, 3), | |
| secNum, | |
| parseFloat(row[4]) || 0, | |
| ...localValues, | |
| parseFloat(row[14]) || 0, | |
| ...foreignValues, | |
| parseFloat(row[24]) || 0, | |
| ttotal, | |
| ...localValues.map(v => secNum > 0 ? v / secNum * 100 : 0), | |
| secNum > 0 ? totalLocal / secNum * 100 : 0, | |
| ...foreignValues.map(v => secNum > 0 ? v / secNum * 100 : 0) | |
| ]; | |
| rows.push({ display: displayRow, numeric: numericRow, code, type }); | |
| }); | |
| return { headers: fullHeaders, rows, types }; | |
| } | |
| function displayTable(data) { | |
| allRows = data.rows; | |
| uniqueTypes = new Set(data.types); | |
| fullHeaders = data.headers; | |
| hiddenColumns.clear(); | |
| let table = '<table id="dataTable"><thead><tr>'; | |
| fullHeaders.forEach((h, idx) => { | |
| const isText = ['Date', 'Code', 'Type'].includes(h); | |
| table += `<th class="${isText ? 'header' : ''}" onclick="sortTable(${idx})">${h}</th>`; | |
| }); | |
| table += '</tr></thead><tbody id="tableBody"></tbody></table>'; | |
| document.getElementById('output').innerHTML = table; | |
| // Isi filter type | |
| const typeFilter = document.getElementById('typeFilter'); | |
| typeFilter.innerHTML = '<option value="">Semua Type</option>'; | |
| [...uniqueTypes].sort().forEach(t => typeFilter.innerHTML += `<option value="${t}">${t}</option>`); | |
| // Tampilkan sidebar | |
| document.getElementById('sidebar').style.display = 'block'; | |
| buildColumnChecklist(); | |
| applyFilters(); | |
| } | |
| function buildColumnChecklist() { | |
| const list = document.getElementById('columnChecklist'); | |
| list.innerHTML = ''; | |
| fullHeaders.forEach((header, idx) => { | |
| const isMain = ['Date', 'Code', 'Type', 'Sec. Num', 'Price'].includes(header); | |
| if (isMain) return; // Kolom utama tidak bisa dihidden | |
| const div = document.createElement('div'); | |
| div.className = 'column-item'; | |
| div.innerHTML = ` | |
| <input type="checkbox" id="col_${idx}" onchange="toggleColumn(${idx}, this.checked)"> | |
| <label for="col_${idx}">${header}</label> | |
| `; | |
| list.appendChild(div); | |
| }); | |
| } | |
| function toggleHideMode() { | |
| hideMode = !hideMode; | |
| document.getElementById('hideModeBtnText').textContent = hideMode ? 'Keluar Mode Hidden' : 'Mode Hidden Kolom'; | |
| document.getElementById('columnChecklist').style.display = hideMode ? 'block' : 'none'; | |
| } | |
| function toggleColumn(colIndex, checked) { | |
| if (checked) { | |
| hiddenColumns.add(colIndex); | |
| } else { | |
| hiddenColumns.delete(colIndex); | |
| } | |
| applyFilters(); | |
| } | |
| function applyFilters() { | |
| const typeFilter = document.getElementById('typeFilter').value; | |
| const searchTerm = document.getElementById('searchInput').value.toUpperCase(); | |
| const tbody = document.getElementById('tableBody'); | |
| tbody.innerHTML = ''; | |
| const filteredRows = allRows.filter(r => { | |
| const matchType = !typeFilter || r.type === typeFilter; | |
| const matchSearch = !searchTerm || r.code.includes(searchTerm); | |
| return matchType && matchSearch; | |
| }); | |
| if (filteredRows.length === 0) { | |
| tbody.innerHTML = `<tr><td colspan="50" class="no-data">Tidak ada data yang sesuai.</td></tr>`; | |
| return; | |
| } | |
| filteredRows.forEach(r => { | |
| const tr = document.createElement('tr'); | |
| r.display.forEach((cell, idx) => { | |
| if (hiddenColumns.has(idx)) return; | |
| const td = document.createElement('td'); | |
| td.className = idx <= 2 ? 'header' : ''; | |
| td.textContent = cell; | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| }); | |
| // Sembunyikan header yang dihidden | |
| document.querySelectorAll('#dataTable thead th').forEach((th, idx) => { | |
| th.style.display = hiddenColumns.has(idx) ? 'none' : ''; | |
| }); | |
| } | |
| let sortDirection = {}; | |
| function sortTable(colIndex) { | |
| if (hiddenColumns.has(colIndex)) return; | |
| sortDirection[colIndex] = sortDirection[colIndex] === undefined ? true : !sortDirection[colIndex]; | |
| const asc = sortDirection[colIndex]; | |
| document.querySelectorAll('#dataTable thead th').forEach(th => th.classList.remove('sorted-asc', 'sorted-desc')); | |
| document.querySelectorAll('#dataTable thead th')[colIndex].classList.add(asc ? 'sorted-asc' : 'sorted-desc'); | |
| const sortedRows = [...allRows].sort((a, b) => { | |
| let va = a.numeric[colIndex]; | |
| let vb = b.numeric[colIndex]; | |
| if (isNaN(va)) { | |
| va = a.display[colIndex]; | |
| vb = b.display[colIndex]; | |
| return asc ? va.localeCompare(vb) : vb.localeCompare(va); | |
| } | |
| return asc ? va - vb : vb - va; | |
| }); | |
| allRows = sortedRows; | |
| applyFilters(); | |
| } | |
| async function loadFile(key) { | |
| const data = await localforage.getItem(key); | |
| if (data) { | |
| currentFileKey = key; | |
| displayTable(data); | |
| openTab('upload'); | |
| } | |
| } | |
| async function renameFile(key) { | |
| const data = await localforage.getItem(key); | |
| const newName = prompt('Masukkan nama baru:', data.name); | |
| if (newName && newName !== data.name) { | |
| data.name = newName; | |
| await localforage.setItem(key, data); | |
| loadSavedFiles(); | |
| } | |
| } | |
| async function deleteFile(key) { | |
| if (confirm('Yakin ingin menghapus file ini?')) { | |
| await localforage.removeItem(key); | |
| loadSavedFiles(); | |
| updateFileCount(); | |
| } | |
| } | |
| async function loadSavedFiles() { | |
| const list = document.getElementById('fileList'); | |
| const noMsg = document.getElementById('noFilesMsg'); | |
| list.innerHTML = ''; | |
| const keys = await localforage.keys(); | |
| const balanceKeys = keys.filter(k => k.startsWith('balance_')).sort((a,b) => b.localeCompare(a)); | |
| noMsg.style.display = balanceKeys.length === 0 ? 'block' : 'none'; | |
| for (const key of balanceKeys) { | |
| const data = await localforage.getItem(key); | |
| const li = document.createElement('li'); | |
| li.innerHTML = ` | |
| <div> | |
| <strong>${data.name}</strong><br> | |
| <small>${data.date}</small> | |
| </div> | |
| <div class="file-actions"> | |
| <button onclick="loadFile('${key}')">Lihat</button> | |
| <button onclick="renameFile('${key}')">Edit Nama</button> | |
| <button class="danger" onclick="deleteFile('${key}')">Hapus</button> | |
| </div> | |
| `; | |
| list.appendChild(li); | |
| } | |
| } | |
| function updateFileCount() { | |
| localforage.keys().then(keys => { | |
| const count = keys.filter(k => k.startsWith('balance_')).length; | |
| document.getElementById('fileCount').textContent = count; | |
| }); | |
| } | |
| updateFileCount(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/balancepos-v2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |