|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|
|
<title>Stock Ownership Analytics Tool</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.css" rel="stylesheet" type="text/css" /> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
body { |
|
|
font-family: 'Inter', sans-serif; |
|
|
} |
|
|
.chart-container { |
|
|
position: relative; |
|
|
height: 350px; |
|
|
width: 100%; |
|
|
} |
|
|
.loading-spinner { |
|
|
border-top-color: #3498db; |
|
|
animation: spinner 1.2s linear infinite; |
|
|
} |
|
|
@keyframes spinner { |
|
|
0% { transform: rotate(0deg); } |
|
|
100% { transform: rotate(360deg); } |
|
|
} |
|
|
.database-item { |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.database-item:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 10px 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen"> |
|
|
|
|
|
|
|
|
<nav class="bg-white shadow-lg sticky top-0 z-50 border-b"> |
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
|
|
<div class="flex justify-between items-center h-16"> |
|
|
<div class="flex items-center"> |
|
|
<div class="text-2xl font-bold text-indigo-600 flex items-center gap-2"> |
|
|
<i class="fas fa-chart-line"></i> |
|
|
<span>StockOwnership Insights</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="hidden md:flex space-x-8"> |
|
|
<a href="#dashboard" class="text-gray-700 hover:text-indigo-600 font-medium">Dashboard</a> |
|
|
<a href="#uploader" class="text-gray-700 hover:text-indigo-600 font-medium">Upload Data</a> |
|
|
<a href="#database" class="text-gray-700 hover:text-indigo-600 font-medium">Database</a> |
|
|
<a href="#download" class="text-gray-700 hover:text-indigo-600 font-medium">Export</a> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</nav> |
|
|
|
|
|
|
|
|
<section class="py-16 px-6 text-center bg-gradient-to-r from-indigo-600 to-purple-600 text-white"> |
|
|
<div class="max-w-4xl mx-auto"> |
|
|
<h1 class="text-4xl md:text-5xl font-bold mb-6 leading-tight"> |
|
|
Analisis Kepemilikan Saham |
|
|
</h1> |
|
|
<p class="text-lg md:text-xl opacity-90 mb-10"> |
|
|
Pantau pertumbuhan kepemilikan saham oleh investor lokal, asing, dan institusi secara real-time. |
|
|
Ekstrak data dari KSEI dan analisis dalam dashboard interaktif. |
|
|
</p> |
|
|
<button onclick="scrollToSection('uploader')" class="bg-white text-indigo-700 font-semibold px-8 py-3 rounded-full shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200 flex items-center gap-2 mx-auto"> |
|
|
<i class="fas fa-file-import"></i> Mulai Analisis |
|
|
</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="uploader" class="py-16 px-6 bg-white"> |
|
|
<div class="max-w-4xl mx-auto"> |
|
|
<div class="text-center mb-10"> |
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-4">Unggah Data KSEI</h2> |
|
|
<p class="text-gray-600"> |
|
|
Unggah file CSV yang diunduh dari <a href="https://web.ksei.co.id/Download/" target="_blank" class="text-indigo-600 hover:underline">KSEI</a>. |
|
|
Format yang didukung: CSV dengan pembatas <strong>|</strong> dan header <strong>Ticker|Nama Saham|Lokal|Asing|Institusi|Tanggal</strong>. |
|
|
</p> |
|
|
</div> |
|
|
|
|
|
<div id="upload-box" class="border-2 border-dashed border-gray-300 rounded-xl p-10 text-center hover:border-indigo-400 transition-colors duration-200 cursor-pointer relative overflow-hidden group"> |
|
|
<input type="file" id="csv-upload" accept=".csv" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" onchange="handleFileSelect(event)" /> |
|
|
<div class="flex flex-col items-center"> |
|
|
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 group-hover:text-indigo-500 transition-colors duration-200"></i> |
|
|
<p class="mt-4 text-gray-600 font-medium">Seret dan lepas file CSV, atau klik untuk memilih</p> |
|
|
<p class="text-sm text-gray-500 mt-2">Format: CSV dengan delimiter |, maks 10MB</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="upload-meta" class="mt-6 bg-gray-50 p-6 rounded-xl hidden"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">Informasi Data</h3> |
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Bulan</label> |
|
|
<select id="upload-month" class="w-full border border-gray-300 rounded-lg px-3 py-2"> |
|
|
<option value="1">Januari</option> |
|
|
<option value="2">Februari</option> |
|
|
<option value="3">Maret</option> |
|
|
<option value="4">April</option> |
|
|
<option value="5">Mei</option> |
|
|
<option value="6">Juni</option> |
|
|
<option value="7">Juli</option> |
|
|
<option value="8">Agustus</option> |
|
|
<option value="9">September</option> |
|
|
<option value="10">Oktober</option> |
|
|
<option value="11">November</option> |
|
|
<option value="12">Desember</option> |
|
|
</select> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Tahun</label> |
|
|
<input type="number" id="upload-year" class="w-full border border-gray-300 rounded-lg px-3 py-2" |
|
|
value="2023" min="2010" max="2030" /> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Nomor Laporan</label> |
|
|
<input type="number" id="upload-number" class="w-full border border-gray-300 rounded-lg px-3 py-2" |
|
|
value="1" min="1" /> |
|
|
</div> |
|
|
</div> |
|
|
<div class="mt-4 flex justify-end"> |
|
|
<button onclick="saveToDatabase()" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition"> |
|
|
Simpan ke Database |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="loading" class="flex flex-col items-center justify-center mt-8 hidden"> |
|
|
<div class="w-16 h-16 border-4 border-gray-200 border-t-indigo-500 rounded-full loading-spinner"></div> |
|
|
<p class="mt-4 text-gray-600">Memproses data...</p> |
|
|
</div> |
|
|
|
|
|
<div id="preview" class="mt-8 hidden"> |
|
|
<h3 class="text-xl font-semibold text-gray-800 mb-4">Pratinjau Data</h3> |
|
|
<div class="overflow-x-auto bg-gray-50 rounded-lg border"> |
|
|
<table id="data-preview-table" class="min-w-full text-sm"></table> |
|
|
</div> |
|
|
<div class="mt-4 flex justify-end"> |
|
|
<button onclick="processData()" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition"> |
|
|
Proses Data |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="database" class="py-16 px-6 bg-gray-50"> |
|
|
<div class="max-w-6xl mx-auto"> |
|
|
<div class="text-center mb-10"> |
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-4">Database Laporan Kepemilikan</h2> |
|
|
<p class="text-gray-600">Kelola dan akses laporan bulanan kepemilikan saham</p> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-10" id="database-container"> |
|
|
|
|
|
<div class="text-center py-10 text-gray-500"> |
|
|
<i class="fas fa-database text-5xl mb-4 opacity-30"></i> |
|
|
<p>Belum ada data dalam database</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="dashboard" class="py-16 px-6 bg-gray-50"> |
|
|
<div class="max-w-7xl mx-auto"> |
|
|
<div class="text-center mb-10"> |
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-4">Dashboard Analitik</h2> |
|
|
<p class="text-gray-600">Analisa perubahan kepemilikan saham secara mendalam</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-10"> |
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<div class="flex items-center"> |
|
|
<div class="p-3 rounded-full bg-blue-100 text-blue-600"> |
|
|
<i class="fas fa-user-friends"></i> |
|
|
</div> |
|
|
<div class="ml-4"> |
|
|
<p class="text-sm font-medium text-gray-600">Total Saham</p> |
|
|
<p id="total-stocks" class="text-2xl font-bold text-gray-800">-</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<div class="flex items-center"> |
|
|
<div class="p-3 rounded-full bg-green-100 text-green-600"> |
|
|
<i class="fas fa-arrow-up"></i> |
|
|
</div> |
|
|
<div class="ml-4"> |
|
|
<p class="text-sm font-medium text-gray-600">Saham Asing Naik</p> |
|
|
<p id="foreign-up" class="text-2xl font-bold text-gray-800">-</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<div class="flex items-center"> |
|
|
<div class="p-3 rounded-full bg-purple-100 text-purple-600"> |
|
|
<i class="fas fa-building"></i> |
|
|
</div> |
|
|
<div class="ml-4"> |
|
|
<p class="text-sm font-medium text-gray-600">Saham Institusi</p> |
|
|
<p id="institutional-total" class="text-2xl font-bold text-gray-800">-</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<div class="flex items-center"> |
|
|
<div class="p-3 rounded-full bg-indigo-100 text-indigo-600"> |
|
|
<i class="fas fa-trending-up"></i> |
|
|
</div> |
|
|
<div class="ml-4"> |
|
|
<p class="text-sm font-medium text-gray-600">Rata-Rata Kenaikan Asing</p> |
|
|
<p id="foreign-growth" class="text-2xl font-bold text-gray-800">-</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10"> |
|
|
|
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">Distribusi Kepemilikan (Rata-rata)</h3> |
|
|
<div class="chart-container"> |
|
|
<canvas id="ownershipChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white p-6 rounded-xl shadow-md border"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">Trend Pertumbuhan Asing</h3> |
|
|
<div class="chart-container"> |
|
|
<canvas id="growthChart"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white p-6 rounded-xl shadow-md border mb-10"> |
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">Saham dengan Kenaikan Kepemilikan Asing Tertinggi</h3> |
|
|
<div class="overflow-x-auto"> |
|
|
<table id="top-movers-table" class="min-w-full text-sm"> |
|
|
<thead> |
|
|
<tr class="border-b"> |
|
|
<th class="py-2 text-left">Ticker</th> |
|
|
<th class="py-2 text-left">Nama</th> |
|
|
<th class="py-2 text-right">Kenaikan (%)</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody></tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section id="download" class="py-16 px-6 bg-white"> |
|
|
<div class="max-w-4xl mx-auto text-center"> |
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-4">Ekspor Hasil Analisis</h2> |
|
|
<p class="text-gray-600 mb-8"> |
|
|
Ekspor data dan hasil analisis ke format CSV untuk disimpan atau digunakan di aplikasi lain. |
|
|
</p> |
|
|
<button onclick="exportAnalysis()" class="bg-green-600 hover:bg-green-700 text-white font-semibold px-8 py-3 rounded-full shadow-lg flex items-center gap-2 mx-auto transition"> |
|
|
<i class="fas fa-file-export"></i> Ekspor ke CSV |
|
|
</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<footer class="py-8 bg-gray-800 text-white text-center"> |
|
|
<div class="max-w-4xl mx-auto px-6"> |
|
|
<p>StockOwnership Insights Tool © 2023 | Data bersumber dari <a href="https://web.ksei.co.id" class="text-indigo-400 hover:underline">KSEI</a></p> |
|
|
<p class="text-sm text-gray-400 mt-2">Alat analisis ini tidak berafiliasi dengan KSEI. Gunakan dengan tanggung jawab Anda sendiri.</p> |
|
|
</div> |
|
|
</footer> |
|
|
|
|
|
<script> |
|
|
|
|
|
let rawData = []; |
|
|
let processedData = []; |
|
|
let ownershipChart = null; |
|
|
let growthChart = null; |
|
|
let database = JSON.parse(localStorage.getItem('ownershipDatabase')) || []; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
renderDatabase(); |
|
|
}); |
|
|
|
|
|
|
|
|
function scrollToSection(id) { |
|
|
document.getElementById(id).scrollIntoView({ behavior: 'smooth' }); |
|
|
} |
|
|
|
|
|
|
|
|
function handleFileSelect(event) { |
|
|
const file = event.target.files[0]; |
|
|
if (!file) return; |
|
|
|
|
|
document.getElementById('loading').classList.remove('hidden'); |
|
|
document.getElementById('preview').classList.add('hidden'); |
|
|
document.getElementById('upload-meta').classList.add('hidden'); |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = function(e) { |
|
|
const csv = e.target.result; |
|
|
Papa.parse(csv, { |
|
|
header: true, |
|
|
skipEmptyLines: true, |
|
|
delimiter: "|", |
|
|
complete: function(results) { |
|
|
rawData = results.data; |
|
|
previewData(rawData.slice(0, 10)); |
|
|
document.getElementById('loading').classList.add('hidden'); |
|
|
document.getElementById('preview').classList.remove('hidden'); |
|
|
document.getElementById('upload-meta').classList.remove('hidden'); |
|
|
}, |
|
|
error: function(error) { |
|
|
alert("Error parsing CSV: " + error); |
|
|
document.getElementById('loading').classList.add('hidden'); |
|
|
} |
|
|
}); |
|
|
}; |
|
|
reader.readAsText(file); |
|
|
} |
|
|
|
|
|
|
|
|
function previewData(data) { |
|
|
const table = document.getElementById('data-preview-table'); |
|
|
table.innerHTML = ''; |
|
|
|
|
|
|
|
|
const thead = document.createElement('thead'); |
|
|
thead.innerHTML = '<tr class="border-b"><th class="py-2 text-left">Ticker</th><th class="py-2 text-left">Nama</th><th class="py-2 text-right">Lokal</th><th class="py-2 text-right">Asing</th><th class="py-2 text-right">Institusi</th><th class="py-2 text-right">Tanggal</th></tr>'; |
|
|
table.appendChild(thead); |
|
|
|
|
|
|
|
|
const tbody = document.createElement('tbody'); |
|
|
data.forEach(row => { |
|
|
const tr = document.createElement('tr'); |
|
|
tr.className = 'border-b hover:bg-gray-50'; |
|
|
tr.innerHTML = ` |
|
|
<td class="py-2 font-mono">${row.Ticker || '-'}</td> |
|
|
<td class="py-2">${row['Nama Saham'] || '-'}</td> |
|
|
<td class="py-2 text-right">${formatNumber(row.Lokal)}</td> |
|
|
<td class="py-2 text-right">${formatNumber(row.Asing)}</td> |
|
|
<td class="py-2 text-right">${formatNumber(row.Institusi)}</td> |
|
|
<td class="py-2 text-right">${row.Tanggal || '-'}</td> |
|
|
`; |
|
|
tbody.appendChild(tr); |
|
|
}); |
|
|
table.appendChild(tbody); |
|
|
} |
|
|
|
|
|
|
|
|
function processData() { |
|
|
|
|
|
processedData = rawData.map(row => ({ |
|
|
...row, |
|
|
Lokal: parseFloat(row.Lokal) || 0, |
|
|
Asing: parseFloat(row.Asing) || 0, |
|
|
Institusi: parseFloat(row.Institusi) || 0 |
|
|
})).filter(row => row.Ticker && !isNaN(row.Asing)); |
|
|
|
|
|
|
|
|
updateMetrics(); |
|
|
|
|
|
|
|
|
renderCharts(); |
|
|
|
|
|
|
|
|
renderTopMovers(); |
|
|
|
|
|
|
|
|
scrollToSection('dashboard'); |
|
|
} |
|
|
|
|
|
|
|
|
function formatNumber(num) { |
|
|
if (typeof num !== 'number') num = parseFloat(num); |
|
|
if (isNaN(num)) return '-'; |
|
|
return num.toLocaleString('id-ID', { maximumFractionDigits: 2 }); |
|
|
} |
|
|
|
|
|
|
|
|
function updateMetrics() { |
|
|
const totalStocks = processedData.length; |
|
|
const foreignUp = processedData.filter(row => row.Asing > 0).length; |
|
|
const institutionalTotal = processedData.reduce((sum, row) => sum + row.Institusi, 0); |
|
|
const avgForeignGrowth = processedData.length > 0 |
|
|
? processedData.reduce((sum, row) => sum + row.Asing, 0) / processedData.length |
|
|
: 0; |
|
|
|
|
|
document.getElementById('total-stocks').textContent = totalStocks; |
|
|
document.getElementById('foreign-up').textContent = foreignUp; |
|
|
document.getElementById('institutional-total').textContent = formatNumber(institutionalTotal); |
|
|
document.getElementById('foreign-growth').textContent = formatNumber(avgForeignGrowth) + '%'; |
|
|
} |
|
|
|
|
|
|
|
|
function renderCharts() { |
|
|
|
|
|
const avgLocal = processedData.reduce((sum, row) => sum + row.Lokal, 0) / processedData.length; |
|
|
const avgForeign = processedData.reduce((sum, row) => sum + row.Asing, 0) / processedData.length; |
|
|
const avgInstitutional = processedData.reduce((sum, row) => sum + row.Institusi, 0) / processedData.length; |
|
|
|
|
|
const ctx1 = document.getElementById('ownershipChart').getContext('2d'); |
|
|
if (ownershipChart) ownershipChart.destroy(); |
|
|
ownershipChart = new Chart(ctx1, { |
|
|
type: 'pie', |
|
|
data: { |
|
|
labels: ['Investor Lokal', 'Investor Asing', 'Institusi'], |
|
|
datasets: [{ |
|
|
data: [avgLocal, avgForeign, avgInstitutional], |
|
|
backgroundColor: ['#6366F1', '#10B981', '#8B5CF6'], |
|
|
borderWidth: 2, |
|
|
borderColor: '#FFFFFF', |
|
|
hoverOffset: 10 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
plugins: { |
|
|
legend: { |
|
|
position: 'bottom' |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const groupedByDate = {}; |
|
|
processedData.forEach(row => { |
|
|
const date = row.Tanggal; |
|
|
if (!groupedByDate[date]) { |
|
|
groupedByDate[date] = { sum: 0, count: 0 }; |
|
|
} |
|
|
groupedByDate[date].sum += parseFloat(row.Asing) || 0; |
|
|
groupedByDate[date].count++; |
|
|
}); |
|
|
|
|
|
const dates = Object.keys(groupedByDate).sort(); |
|
|
const avgGrowth = dates.map(date => groupedByDate[date].sum / groupedByDate[date].count); |
|
|
|
|
|
const ctx2 = document.getElementById('growthChart').getContext('2d'); |
|
|
if (growthChart) growthChart.destroy(); |
|
|
growthChart = new Chart(ctx2, { |
|
|
type: 'line', |
|
|
data: { |
|
|
labels: dates, |
|
|
datasets: [{ |
|
|
label: 'Rata-rata Kepemilikan Asing (%)', |
|
|
data: avgGrowth, |
|
|
borderColor: '#EC4899', |
|
|
backgroundColor: 'rgba(236, 72, 153, 0.1)', |
|
|
fill: true, |
|
|
tension: 0.4, |
|
|
pointBackgroundColor: '#EC4899' |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
plugins: { |
|
|
legend: { |
|
|
display: false |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
y: { |
|
|
beginAtZero: true, |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Pertumbuhan (%)' |
|
|
} |
|
|
}, |
|
|
x: { |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Tanggal' |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function renderTopMovers() { |
|
|
const sorted = [...processedData] |
|
|
.sort((a, b) => (parseFloat(b.Asing) || 0) - (parseFloat(a.Asing) || 0)) |
|
|
.slice(0, 10); |
|
|
|
|
|
const table = document.getElementById('top-movers-table').getElementsByTagName('tbody')[0]; |
|
|
table.innerHTML = ''; |
|
|
|
|
|
sorted.forEach(row => { |
|
|
const tr = document.createElement('tr'); |
|
|
tr.className = 'border-b hover:bg-gray-50'; |
|
|
tr.innerHTML = ` |
|
|
<td class="py-2 font-mono font-semibold">${row.Ticker}</td> |
|
|
<td class="py-2">${row['Nama Saham'] || '-'}</td> |
|
|
<td class="py-2 text-right text-green-600 font-semibold">${formatNumber(row.Asing)}%</td> |
|
|
`; |
|
|
table.appendChild(tr); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function saveToDatabase() { |
|
|
if (rawData.length === 0) { |
|
|
alert('Belum ada data untuk disimpan.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const month = document.getElementById('upload-month').value; |
|
|
const year = document.getElementById('upload-year').value; |
|
|
const number = document.getElementById('upload-number').value; |
|
|
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", |
|
|
"Juli", "Agustus", "September", "Oktober", "November", "Desember"]; |
|
|
|
|
|
const entry = { |
|
|
id: Date.now(), |
|
|
month: parseInt(month), |
|
|
year: parseInt(year), |
|
|
number: parseInt(number), |
|
|
monthName: monthNames[month - 1], |
|
|
date: new Date().toISOString(), |
|
|
recordCount: rawData.length |
|
|
}; |
|
|
|
|
|
database.push(entry); |
|
|
database.sort((a, b) => new Date(b.year, b.month) - new Date(a.year, a.month)); |
|
|
|
|
|
localStorage.setItem('ownershipDatabase', JSON.stringify(database)); |
|
|
renderDatabase(); |
|
|
|
|
|
alert(`Data berhasil disimpan ke database!`); |
|
|
} |
|
|
|
|
|
|
|
|
function renderDatabase() { |
|
|
const container = document.getElementById('database-container'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
if (database.length === 0) { |
|
|
container.innerHTML = ` |
|
|
<div class="text-center py-10 text-gray-500 col-span-2"> |
|
|
<i class="fas fa-database text-5xl mb-4 opacity-30"></i> |
|
|
<p>Belum ada data dalam database</p> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
database.forEach(item => { |
|
|
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", |
|
|
"Juli", "Agustus", "September", "Oktober", "November", "Desember"]; |
|
|
|
|
|
const itemDiv = document.createElement('div'); |
|
|
itemDiv.className = 'bg-white p-6 rounded-xl shadow-md border database-item'; |
|
|
itemDiv.innerHTML = ` |
|
|
<div class="flex justify-between items-start"> |
|
|
<div> |
|
|
<span class="inline-block bg-indigo-100 text-indigo-800 text-xs px-3 py-1 rounded-full font-medium"> |
|
|
No. ${item.number} |
|
|
</span> |
|
|
<h3 class="text-xl font-bold text-gray-800 mt-2">${item.monthName} ${item.year}</h3> |
|
|
<p class="text-gray-600">Diunggah: ${new Date(item.date).toLocaleDateString('id-ID')}</p> |
|
|
</div> |
|
|
<div class="text-right"> |
|
|
<span class="inline-block bg-gray-100 text-gray-800 text-sm px-3 py-1 rounded-full"> |
|
|
${item.recordCount} saham |
|
|
</span> |
|
|
<div class="mt-2 space-x-2"> |
|
|
<button onclick="loadFromDatabase(${item.id})" class="text-blue-600 hover:text-blue-800 text-sm"> |
|
|
<i class="fas fa-chart-bar"></i> Lihat |
|
|
</button> |
|
|
<button onclick="deleteFromDatabase(${item.id})" class="text-red-600 hover:text-red-800 text-sm"> |
|
|
<i class="fas fa-trash"></i> Hapus |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
container.appendChild(itemDiv); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function loadFromDatabase(id) { |
|
|
const item = database.find(d => d.id === id); |
|
|
if (!item) return; |
|
|
|
|
|
alert(`Fitur lengkap akan diintegrasikan. Saat ini, fungsi ini akan memuat data laporan ${item.monthName} ${item.year}`); |
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function deleteFromDatabase(id) { |
|
|
if (confirm('Apakah Anda yakin ingin menghapus laporan ini dari database?')) { |
|
|
database = database.filter(d => d.id !== id); |
|
|
localStorage.setItem('ownershipDatabase', JSON.stringify(database)); |
|
|
renderDatabase(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function exportAnalysis() { |
|
|
if (processedData.length === 0) { |
|
|
alert('Tidak ada data untuk diekspor. Silakan unggah dan proses data terlebih dahulu.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const exportData = processedData.map(row => ({ |
|
|
'Ticker': row.Ticker, |
|
|
'Nama Saham': row['Nama Saham'], |
|
|
'Kepemilikan Lokal (%)': formatNumber(row.Lokal), |
|
|
'Kepemilikan Asing (%)': formatNumber(row.Asing), |
|
|
'Kepemilikan Institusi (%)': formatNumber(row.Institusi), |
|
|
'Tanggal': row.Tanggal, |
|
|
'Kategori': row.Asing > 0 ? 'Net Buy' : 'Net Sell' |
|
|
})); |
|
|
|
|
|
|
|
|
const csv = Papa.unparse(exportData); |
|
|
|
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const link = document.createElement('a'); |
|
|
link.setAttribute('href', url); |
|
|
link.setAttribute('download', 'analisis-kepemilikan-saham.csv'); |
|
|
link.style.visibility = 'hidden'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
} |
|
|
</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/ksei" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |