Skip to content

Create initial HTML file with styles - #77

Closed
Sazwan gibhub (Sazwan1996) wants to merge 1 commit into
MicrosoftDocs:mainfrom
Sazwan1996:patch-2
Closed

Create initial HTML file with styles#77
Sazwan gibhub (Sazwan1996) wants to merge 1 commit into
MicrosoftDocs:mainfrom
Sazwan1996:patch-2

Conversation

@Sazwan1996

Copy link
Copy Markdown

Add HTML structure and styles for the application.tambahkan data semuanya,sync_portfolio.pyrequest.url.pathws.onmessagews.onmessagegoogle.cloudbigquery_client.pyledger_doc.existsredis.asynciofastapi.middleware.corsmain.pysgae.identityNext.jsqualityReport.innerHTMLstatsContainer.innerHTMLpageInfo.textContentfilteredData.lengthfilteredRowsLabel.textContentfullData.lengthtotalRowsLabel.textContenttableBody.innerHTMLtableHeader.innerHTMLparsed.lengthrowCountDisplay.textContentfileNameDisplay.textContentreader.onloadfileInfo.style.displayuploadPrompt.style.displayfileInput.valueuploadArea.style.borderColorobj.fileNameobj.data.lengthdarkToggle.innerHTMLChart.js

<title>⚡ Data Dashboard Power</title> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> <style> :root { --bg-body: #f4f6f9; --bg-card: #ffffff; --text-color: #212529; --border-color: #dee2e6; --shadow: 0 8px 24px rgba(0,0,0,0.08); } [data-theme="dark"] { --bg-body: #1a1d23; --bg-card: #2c3038; --text-color: #e9ecef; --border-color: #495057; --shadow: 0 8px 24px rgba(0,0,0,0.4); } body { background-color: var(--bg-body); color: var(--text-color); transition: background 0.3s, color 0.3s; padding: 1.5rem; } .card { background-color: var(--bg-card); border: 1px solid var(--border-color); box-shadow: var(--shadow); transition: background 0.3s, border 0.3s; border-radius: 16px; } .table { color: var(--text-color); font-size: 0.85rem; } .table th { background: #0d6efd; color: white; position: sticky; top: 0; z-index: 10; white-space: nowrap; } .table td { vertical-align: middle; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .table td .cell-full { display: none; white-space: normal; word-break: break-all; } .table td:hover .cell-full { display: block; } .table td:hover .cell-preview { display: none; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.5rem; } .stats-item { background: var(--bg-body); padding: 0.5rem 0.75rem; border-radius: 8px; border-left: 4px solid #0d6efd; } .chart-container { position: relative; height: 250px; width: 100%; } .btn-icon { margin-right: 4px; } #uploadArea { border: 2px dashed var(--border-color); border-radius: 16px; padding: 2.5rem; text-align: center; cursor: pointer; transition: 0.3s; } #uploadArea:hover { background: rgba(13, 110, 253, 0.05); border-color: #0d6efd; } .dark-toggle { cursor: pointer; font-size: 1.5rem; } .badge-count { background: #0d6efd; color: white; padding: 0.25rem 0.75rem; border-radius: 20px; } media (max-width: 768px) { body { padding: 0.5rem; } .table-wrapper { overflow-x: auto; } } </style>

Data Dashboard Power

Muat naik, analisis, eksport — semua dalam satu HTML

Reset
<!-- Upload Area -->
<div class="card p-4 mb-4" id="uploadArea">
    <div id="uploadPrompt">
        <i class="fas fa-cloud-upload-alt fa-3x text-primary mb-2"></i>
        <h5>Drag & drop atau klik untuk muat naik</h5>
        <p class="text-muted small">Sokongan: CSV, JSON (maks 5MB)</p>
        <input type="file" id="fileInput" accept=".csv,.json" style="display:none;">
    </div>
    <div id="fileInfo" style="display:none;">
        <i class="fas fa-file-alt fa-2x text-success me-2"></i>
        <span id="fileNameDisplay" class="fw-bold"></span>
        <span class="badge bg-secondary ms-2" id="rowCountDisplay">0 baris</span>
        <button class="btn btn-sm btn-outline-secondary ms-3" id="changeFileBtn"><i class="fas fa-exchange-alt"></i> Tukar fail</button>
    </div>
</div>

<!-- Stats & Quality Row -->
<div class="row g-4 mb-4">
    <div class="col-md-8">
        <div class="card p-3">
            <h6><i class="fas fa-chart-simple text-primary"></i> Ringkasan Lajur</h6>
            <div id="statsContainer" class="stats-grid"></div>
        </div>
    </div>
    <div class="col-md-4">
        <div class="card p-3">
            <h6><i class="fas fa-clipboard-check text-success"></i> Laporan Kualiti</h6>
            <div id="qualityReport">
                <p class="text-muted">Tiada data.</p>
            </div>
        </div>
    </div>
</div>

<!-- Charts -->
<div class="card p-3 mb-4">
    <h6><i class="fas fa-chart-bar text-info"></i> Visualisasi Automatik</h6>
    <div class="row g-3" id="chartContainer">
        <div class="col-md-6"><canvas id="chart1"></canvas></div>
        <div class="col-md-6"><canvas id="chart2"></canvas></div>
    </div>
</div>

<!-- Table Controls -->
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
    <div>
        <span class="badge bg-primary rounded-pill me-2" id="totalRowsLabel">0 baris</span>
        <span class="badge bg-secondary rounded-pill" id="filteredRowsLabel">0 ditapis</span>
    </div>
    <div class="d-flex flex-wrap gap-2">
        <input type="text" class="form-control form-control-sm" id="globalSearch" placeholder="Cari semua..." style="width:180px;">
        <button class="btn btn-sm btn-success" id="exportCsvBtn"><i class="fas fa-file-csv"></i> CSV</button>
        <button class="btn btn-sm btn-warning" id="exportExcelBtn"><i class="fas fa-file-excel"></i> Excel</button>
        <button class="btn btn-sm btn-info text-white" id="exportHtmlBtn"><i class="fas fa-code"></i> HTML</button>
    </div>
</div>

<!-- Table -->
<div class="card p-0 overflow-hidden">
    <div class="table-wrapper" style="max-height:500px; overflow-y:auto;">
        <table class="table table-hover table-striped mb-0" id="dataTable">
            <thead>
                <tr id="tableHeader"></tr>
            </thead>
            <tbody id="tableBody"></tbody>
        </table>
    </div>
    <!-- Pagination -->
    <div class="d-flex justify-content-between align-items-center p-3 border-top">
        <span id="pageInfo" class="small">Halaman 1 / 1</span>
        <div>
            <button class="btn btn-outline-primary btn-sm" id="prevPage"><i class="fas fa-chevron-left"></i></button>
            <button class="btn btn-outline-primary btn-sm" id="nextPage"><i class="fas fa-chevron-right"></i></button>
        </div>
    </div>
</div>

<div class="mt-4 text-center text-muted small">
    <i class="fas fa-database"></i> Data disimpan secara automatik di localStorage pelayar anda.
</div>
<script> // ============================================================ // STATE // ============================================================ let fullData = []; let filteredData = []; let columns = []; let currentPage = 1; const rowsPerPage = 10; let sortCol = null; let sortAsc = true; let columnFilters = {}; // { colName: searchString } let chartInstances = { chart1: null, chart2: null }; let currentFileName = ''; const STORAGE_KEY = 'power_dashboard_data'; // ============================================================ // DOM REFS // ============================================================ const fileInput = document.getElementById('fileInput'); const uploadArea = document.getElementById('uploadArea'); const uploadPrompt = document.getElementById('uploadPrompt'); const fileInfo = document.getElementById('fileInfo'); const fileNameDisplay = document.getElementById('fileNameDisplay'); const rowCountDisplay = document.getElementById('rowCountDisplay'); const changeFileBtn = document.getElementById('changeFileBtn'); const tableHeader = document.getElementById('tableHeader'); const tableBody = document.getElementById('tableBody'); const statsContainer = document.getElementById('statsContainer'); const qualityReport = document.getElementById('qualityReport'); const totalRowsLabel = document.getElementById('totalRowsLabel'); const filteredRowsLabel = document.getElementById('filteredRowsLabel'); const pageInfo = document.getElementById('pageInfo'); const prevPage = document.getElementById('prevPage'); const nextPage = document.getElementById('nextPage'); const globalSearch = document.getElementById('globalSearch'); const clearStorageBtn = document.getElementById('clearStorageBtn'); const darkToggle = document.getElementById('darkToggle'); const exportCsvBtn = document.getElementById('exportCsvBtn'); const exportExcelBtn = document.getElementById('exportExcelBtn'); const exportHtmlBtn = document.getElementById('exportHtmlBtn'); // ============================================================ // DARK MODE // ============================================================ let darkMode = localStorage.getItem('power_dashboard_dark') === 'true'; if (darkMode) document.documentElement.setAttribute('data-theme', 'dark'); darkToggle.innerHTML = darkMode ? '' : ''; darkToggle.addEventListener('click', () => { darkMode = !darkMode; localStorage.setItem('power_dashboard_dark', darkMode); document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : ''); darkToggle.innerHTML = darkMode ? '' : ''; }); // ============================================================ // LOCAL STORAGE // ============================================================ function saveToStorage() { if (fullData.length > 0) { localStorage.setItem(STORAGE_KEY, JSON.stringify({ data: fullData, fileName: currentFileName })); } } function loadFromStorage() { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { try { const obj = JSON.parse(raw); if (obj.data && obj.data.length > 0) { fullData = obj.data; currentFileName = obj.fileName || 'Disimpan'; return true; } } catch(e) {} } return false; } // ============================================================ // UPLOAD // ============================================================ uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#0d6efd'; }); uploadArea.addEventListener('dragleave', () => { uploadArea.style.borderColor = 'var(--border-color)'; }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.style.borderColor = 'var(--border-color)'; if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', (e) => { if (e.target.files.length) handleFile(e.target.files[0]); fileInput.value = ''; }); changeFileBtn.addEventListener('click', () => { fullData = []; filteredData = []; columns = []; currentFileName = ''; localStorage.removeItem(STORAGE_KEY); renderAll(); uploadPrompt.style.display = 'block'; fileInfo.style.display = 'none'; }); function handleFile(file) { const ext = file.name.split('.').pop().toLowerCase(); currentFileName = file.name; const reader = new FileReader(); reader.onload = (e) => { let parsed = []; if (ext === 'csv') { const result = Papa.parse(e.target.result, { header: true, dynamicTyping: true, skipEmptyLines: true }); if (result.errors.length > 0) { alert('Ralat CSV: ' + result.errors[0].message); return; } parsed = result.data; } else if (ext === 'json') { try { parsed = JSON.parse(e.target.result); } catch(err) { alert('JSON tidak sah: ' + err.message); return; } if (!Array.isArray(parsed)) { alert('JSON mestilah array objek.'); return; } } else { alert('Sila muat naik CSV atau JSON.'); return; } if (parsed.length === 0) { alert('Fail kosong.'); return; } fullData = parsed; saveToStorage(); renderAll(); uploadPrompt.style.display = 'none'; fileInfo.style.display = 'block'; fileNameDisplay.textContent = file.name; rowCountDisplay.textContent = parsed.length + ' baris'; }; reader.readAsText(file); } // ============================================================ // FILTER, SORT, PAGINATE // ============================================================ function processData() { let data = [...fullData]; if (data.length === 0) { filteredData = []; columns = []; return; } columns = Object.keys(data[0]); // Global search const global = globalSearch.value.toLowerCase().trim(); if (global) { data = data.filter(row => { return columns.some(col => { const val = row[col]; if (val === null || val === undefined) return false; return String(val).toLowerCase().includes(global); }); }); } // Column filters for (const [col, search] of Object.entries(columnFilters)) { if (search && search.trim() !== '') { const term = search.trim().toLowerCase(); data = data.filter(row => { const val = row[col]; if (val === null || val === undefined) return false; return String(val).toLowerCase().includes(term); }); } } // Sort if (sortCol) { data.sort((a, b) => { const va = a[sortCol] ?? ''; const vb = b[sortCol] ?? ''; if (typeof va === 'number' && typeof vb === 'number') { return sortAsc ? va - vb : vb - va; } const sa = String(va).toLowerCase(); const sb = String(vb).toLowerCase(); return sortAsc ? sa.localeCompare(sb) : sb.localeCompare(sa); }); } filteredData = data; } function getPaginatedData() { const start = (currentPage - 1) * rowsPerPage; return filteredData.slice(start, start + rowsPerPage); } function totalPages() { return Math.ceil(filteredData.length / rowsPerPage) || 1; } // ============================================================ // RENDER TABLE // ============================================================ function renderTable() { if (fullData.length === 0) { tableHeader.innerHTML = ''; tableBody.innerHTML = 'Tiada data. Muat naik fail CSV/JSON.'; return; } // Header with sort let headerHtml = ''; for (const col of columns) { let sortIcon = ''; if (sortCol === col) sortIcon = sortAsc ? ' ▲' : ' ▼'; headerHtml += `${col}${sortIcon}`; } headerHtml += ''; tableHeader.innerHTML = headerHtml; // Click sort tableHeader.querySelectorAll('th').forEach(th => { th.addEventListener('click', () => { const col = th.dataset.col; if (sortCol === col) sortAsc = !sortAsc; else { sortCol = col; sortAsc = true; } currentPage = 1; renderAll(); }); }); // Body const paginated = getPaginatedData(); if (paginated.length === 0) { tableBody.innerHTML = 'Tiada rekod sepadan.'; return; } let bodyHtml = ''; for (const row of paginated) { bodyHtml += ''; for (const col of columns) { let val = row[col]; if (val === null || val === undefined) val = ''; const display = String(val); bodyHtml += ` ${display.length > 30 ? display.substring(0,30)+'…' : display} ${display} `; } bodyHtml += ''; } tableBody.innerHTML = bodyHtml; // Update labels totalRowsLabel.textContent = fullData.length + ' baris'; filteredRowsLabel.textContent = filteredData.length + ' ditapis'; const tp = totalPages(); pageInfo.textContent = `Halaman ${currentPage} / ${tp}`; } // ============================================================ // RENDER STATS & QUALITY // ============================================================ function renderStats() { if (fullData.length === 0) { statsContainer.innerHTML = 'Tiada data.'; qualityReport.innerHTML = '

Tiada data.

'; return; } const

Add HTML structure and styles for the application.tambahkan data semuanya,sync_portfolio.pyrequest.url.pathws.onmessagews.onmessagegoogle.cloudbigquery_client.pyledger_doc.existsredis.asynciofastapi.middleware.corsmain.pysgae.identityNext.jsqualityReport.innerHTMLstatsContainer.innerHTMLpageInfo.textContentfilteredData.lengthfilteredRowsLabel.textContentfullData.lengthtotalRowsLabel.textContenttableBody.innerHTMLtableHeader.innerHTMLparsed.lengthrowCountDisplay.textContentfileNameDisplay.textContentreader.onloadfileInfo.style.displayuploadPrompt.style.displayfileInput.valueuploadArea.style.borderColorobj.fileNameobj.data.lengthdarkToggle.innerHTMLChart.js<!DOCTYPE html>
<html lang="ms">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>⚡ Data Dashboard Power</title>
    <!-- Bootstrap 5 -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Font Awesome 6 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
    <!-- Papa Parse (CSV) -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
    <!-- SheetJS (Excel) -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
    <style>
        :root {
            --bg-body: #f4f6f9;
            --bg-card: #ffffff;
            --text-color: #212529;
            --border-color: #dee2e6;
            --shadow: 0 8px 24px rgba(0,0,0,0.08);
        }
        [data-theme="dark"] {
            --bg-body: #1a1d23;
            --bg-card: #2c3038;
            --text-color: #e9ecef;
            --border-color: #495057;
            --shadow: 0 8px 24px rgba(0,0,0,0.4);
        }
        body {
            background-color: var(--bg-body);
            color: var(--text-color);
            transition: background 0.3s, color 0.3s;
            padding: 1.5rem;
        }
        .card {
            background-color: var(--bg-card);
            border: 1px solid var(--border-color);
            box-shadow: var(--shadow);
            transition: background 0.3s, border 0.3s;
            border-radius: 16px;
        }
        .table {
            color: var(--text-color);
            font-size: 0.85rem;
        }
        .table th {
            background: #0d6efd;
            color: white;
            position: sticky;
            top: 0;
            z-index: 10;
            white-space: nowrap;
        }
        .table td {
            vertical-align: middle;
            max-width: 200px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .table td .cell-full {
            display: none;
            white-space: normal;
            word-break: break-all;
        }
        .table td:hover .cell-full {
            display: block;
        }
        .table td:hover .cell-preview {
            display: none;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 0.5rem;
        }
        .stats-item {
            background: var(--bg-body);
            padding: 0.5rem 0.75rem;
            border-radius: 8px;
            border-left: 4px solid #0d6efd;
        }
        .chart-container {
            position: relative;
            height: 250px;
            width: 100%;
        }
        .btn-icon {
            margin-right: 4px;
        }
        #uploadArea {
            border: 2px dashed var(--border-color);
            border-radius: 16px;
            padding: 2.5rem;
            text-align: center;
            cursor: pointer;
            transition: 0.3s;
        }
        #uploadArea:hover {
            background: rgba(13, 110, 253, 0.05);
            border-color: #0d6efd;
        }
        .dark-toggle {
            cursor: pointer;
            font-size: 1.5rem;
        }
        .badge-count {
            background: #0d6efd;
            color: white;
            padding: 0.25rem 0.75rem;
            border-radius: 20px;
        }
        @media (max-width: 768px) {
            body { padding: 0.5rem; }
            .table-wrapper { overflow-x: auto; }
        }
    </style>
</head>
<body>

<div class="container-fluid">
    <!-- Header -->
    <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap">
        <div>
            <h1 class="display-6 fw-bold"><i class="fas fa-bolt text-warning"></i> Data Dashboard Power</h1>
            <p class="text-muted">Muat naik, analisis, eksport — semua dalam satu HTML</p>
        </div>
        <div>
            <span class="dark-toggle me-3" id="darkToggle" title="Tukar mod gelap/terang">
                <i class="fas fa-moon"></i>
            </span>
            <button class="btn btn-outline-danger" id="clearStorageBtn"><i class="fas fa-trash"></i> Reset</button>
        </div>
    </div>

    <!-- Upload Area -->
    <div class="card p-4 mb-4" id="uploadArea">
        <div id="uploadPrompt">
            <i class="fas fa-cloud-upload-alt fa-3x text-primary mb-2"></i>
            <h5>Drag & drop atau klik untuk muat naik</h5>
            <p class="text-muted small">Sokongan: CSV, JSON (maks 5MB)</p>
            <input type="file" id="fileInput" accept=".csv,.json" style="display:none;">
        </div>
        <div id="fileInfo" style="display:none;">
            <i class="fas fa-file-alt fa-2x text-success me-2"></i>
            <span id="fileNameDisplay" class="fw-bold"></span>
            <span class="badge bg-secondary ms-2" id="rowCountDisplay">0 baris</span>
            <button class="btn btn-sm btn-outline-secondary ms-3" id="changeFileBtn"><i class="fas fa-exchange-alt"></i> Tukar fail</button>
        </div>
    </div>

    <!-- Stats & Quality Row -->
    <div class="row g-4 mb-4">
        <div class="col-md-8">
            <div class="card p-3">
                <h6><i class="fas fa-chart-simple text-primary"></i> Ringkasan Lajur</h6>
                <div id="statsContainer" class="stats-grid"></div>
            </div>
        </div>
        <div class="col-md-4">
            <div class="card p-3">
                <h6><i class="fas fa-clipboard-check text-success"></i> Laporan Kualiti</h6>
                <div id="qualityReport">
                    <p class="text-muted">Tiada data.</p>
                </div>
            </div>
        </div>
    </div>

    <!-- Charts -->
    <div class="card p-3 mb-4">
        <h6><i class="fas fa-chart-bar text-info"></i> Visualisasi Automatik</h6>
        <div class="row g-3" id="chartContainer">
            <div class="col-md-6"><canvas id="chart1"></canvas></div>
            <div class="col-md-6"><canvas id="chart2"></canvas></div>
        </div>
    </div>

    <!-- Table Controls -->
    <div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
        <div>
            <span class="badge bg-primary rounded-pill me-2" id="totalRowsLabel">0 baris</span>
            <span class="badge bg-secondary rounded-pill" id="filteredRowsLabel">0 ditapis</span>
        </div>
        <div class="d-flex flex-wrap gap-2">
            <input type="text" class="form-control form-control-sm" id="globalSearch" placeholder="Cari semua..." style="width:180px;">
            <button class="btn btn-sm btn-success" id="exportCsvBtn"><i class="fas fa-file-csv"></i> CSV</button>
            <button class="btn btn-sm btn-warning" id="exportExcelBtn"><i class="fas fa-file-excel"></i> Excel</button>
            <button class="btn btn-sm btn-info text-white" id="exportHtmlBtn"><i class="fas fa-code"></i> HTML</button>
        </div>
    </div>

    <!-- Table -->
    <div class="card p-0 overflow-hidden">
        <div class="table-wrapper" style="max-height:500px; overflow-y:auto;">
            <table class="table table-hover table-striped mb-0" id="dataTable">
                <thead>
                    <tr id="tableHeader"></tr>
                </thead>
                <tbody id="tableBody"></tbody>
            </table>
        </div>
        <!-- Pagination -->
        <div class="d-flex justify-content-between align-items-center p-3 border-top">
            <span id="pageInfo" class="small">Halaman 1 / 1</span>
            <div>
                <button class="btn btn-outline-primary btn-sm" id="prevPage"><i class="fas fa-chevron-left"></i></button>
                <button class="btn btn-outline-primary btn-sm" id="nextPage"><i class="fas fa-chevron-right"></i></button>
            </div>
        </div>
    </div>

    <div class="mt-4 text-center text-muted small">
        <i class="fas fa-database"></i> Data disimpan secara automatik di localStorage pelayar anda.
    </div>
</div>

<script>
    // ============================================================
    //  STATE
    // ============================================================
    let fullData = [];
    let filteredData = [];
    let columns = [];
    let currentPage = 1;
    const rowsPerPage = 10;
    let sortCol = null;
    let sortAsc = true;
    let columnFilters = {}; // { colName: searchString }
    let chartInstances = { chart1: null, chart2: null };
    let currentFileName = '';

    const STORAGE_KEY = 'power_dashboard_data';

    // ============================================================
    //  DOM REFS
    // ============================================================
    const fileInput = document.getElementById('fileInput');
    const uploadArea = document.getElementById('uploadArea');
    const uploadPrompt = document.getElementById('uploadPrompt');
    const fileInfo = document.getElementById('fileInfo');
    const fileNameDisplay = document.getElementById('fileNameDisplay');
    const rowCountDisplay = document.getElementById('rowCountDisplay');
    const changeFileBtn = document.getElementById('changeFileBtn');
    const tableHeader = document.getElementById('tableHeader');
    const tableBody = document.getElementById('tableBody');
    const statsContainer = document.getElementById('statsContainer');
    const qualityReport = document.getElementById('qualityReport');
    const totalRowsLabel = document.getElementById('totalRowsLabel');
    const filteredRowsLabel = document.getElementById('filteredRowsLabel');
    const pageInfo = document.getElementById('pageInfo');
    const prevPage = document.getElementById('prevPage');
    const nextPage = document.getElementById('nextPage');
    const globalSearch = document.getElementById('globalSearch');
    const clearStorageBtn = document.getElementById('clearStorageBtn');
    const darkToggle = document.getElementById('darkToggle');
    const exportCsvBtn = document.getElementById('exportCsvBtn');
    const exportExcelBtn = document.getElementById('exportExcelBtn');
    const exportHtmlBtn = document.getElementById('exportHtmlBtn');

    // ============================================================
    //  DARK MODE
    // ============================================================
    let darkMode = localStorage.getItem('power_dashboard_dark') === 'true';
    if (darkMode) document.documentElement.setAttribute('data-theme', 'dark');
    darkToggle.innerHTML = darkMode ? '<i class="fas fa-sun"></i>' : '<i class="fas fa-moon"></i>';

    darkToggle.addEventListener('click', () => {
        darkMode = !darkMode;
        localStorage.setItem('power_dashboard_dark', darkMode);
        document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : '');
        darkToggle.innerHTML = darkMode ? '<i class="fas fa-sun"></i>' : '<i class="fas fa-moon"></i>';
    });

    // ============================================================
    //  LOCAL STORAGE
    // ============================================================
    function saveToStorage() {
        if (fullData.length > 0) {
            localStorage.setItem(STORAGE_KEY, JSON.stringify({ data: fullData, fileName: currentFileName }));
        }
    }

    function loadFromStorage() {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (raw) {
            try {
                const obj = JSON.parse(raw);
                if (obj.data && obj.data.length > 0) {
                    fullData = obj.data;
                    currentFileName = obj.fileName || 'Disimpan';
                    return true;
                }
            } catch(e) {}
        }
        return false;
    }

    // ============================================================
    //  UPLOAD
    // ============================================================
    uploadArea.addEventListener('click', () => fileInput.click());
    uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.style.borderColor = '#0d6efd'; });
    uploadArea.addEventListener('dragleave', () => { uploadArea.style.borderColor = 'var(--border-color)'; });
    uploadArea.addEventListener('drop', (e) => {
        e.preventDefault();
        uploadArea.style.borderColor = 'var(--border-color)';
        if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
    });

    fileInput.addEventListener('change', (e) => {
        if (e.target.files.length) handleFile(e.target.files[0]);
        fileInput.value = '';
    });

    changeFileBtn.addEventListener('click', () => {
        fullData = [];
        filteredData = [];
        columns = [];
        currentFileName = '';
        localStorage.removeItem(STORAGE_KEY);
        renderAll();
        uploadPrompt.style.display = 'block';
        fileInfo.style.display = 'none';
    });

    function handleFile(file) {
        const ext = file.name.split('.').pop().toLowerCase();
        currentFileName = file.name;
        const reader = new FileReader();
        reader.onload = (e) => {
            let parsed = [];
            if (ext === 'csv') {
                const result = Papa.parse(e.target.result, { header: true, dynamicTyping: true, skipEmptyLines: true });
                if (result.errors.length > 0) {
                    alert('Ralat CSV: ' + result.errors[0].message);
                    return;
                }
                parsed = result.data;
            } else if (ext === 'json') {
                try { parsed = JSON.parse(e.target.result); }
                catch(err) { alert('JSON tidak sah: ' + err.message); return; }
                if (!Array.isArray(parsed)) { alert('JSON mestilah array objek.'); return; }
            } else {
                alert('Sila muat naik CSV atau JSON.');
                return;
            }
            if (parsed.length === 0) { alert('Fail kosong.'); return; }
            fullData = parsed;
            saveToStorage();
            renderAll();
            uploadPrompt.style.display = 'none';
            fileInfo.style.display = 'block';
            fileNameDisplay.textContent = file.name;
            rowCountDisplay.textContent = parsed.length + ' baris';
        };
        reader.readAsText(file);
    }

    // ============================================================
    //  FILTER, SORT, PAGINATE
    // ============================================================
    function processData() {
        let data = [...fullData];
        if (data.length === 0) { filteredData = []; columns = []; return; }

        columns = Object.keys(data[0]);

        // Global search
        const global = globalSearch.value.toLowerCase().trim();
        if (global) {
            data = data.filter(row => {
                return columns.some(col => {
                    const val = row[col];
                    if (val === null || val === undefined) return false;
                    return String(val).toLowerCase().includes(global);
                });
            });
        }

        // Column filters
        for (const [col, search] of Object.entries(columnFilters)) {
            if (search && search.trim() !== '') {
                const term = search.trim().toLowerCase();
                data = data.filter(row => {
                    const val = row[col];
                    if (val === null || val === undefined) return false;
                    return String(val).toLowerCase().includes(term);
                });
            }
        }

        // Sort
        if (sortCol) {
            data.sort((a, b) => {
                const va = a[sortCol] ?? '';
                const vb = b[sortCol] ?? '';
                if (typeof va === 'number' && typeof vb === 'number') {
                    return sortAsc ? va - vb : vb - va;
                }
                const sa = String(va).toLowerCase();
                const sb = String(vb).toLowerCase();
                return sortAsc ? sa.localeCompare(sb) : sb.localeCompare(sa);
            });
        }

        filteredData = data;
    }

    function getPaginatedData() {
        const start = (currentPage - 1) * rowsPerPage;
        return filteredData.slice(start, start + rowsPerPage);
    }

    function totalPages() {
        return Math.ceil(filteredData.length / rowsPerPage) || 1;
    }

    // ============================================================
    //  RENDER TABLE
    // ============================================================
    function renderTable() {
        if (fullData.length === 0) {
            tableHeader.innerHTML = '';
            tableBody.innerHTML = '<tr><td class="text-center text-muted">Tiada data. Muat naik fail CSV/JSON.</td></tr>';
            return;
        }

        // Header with sort
        let headerHtml = '<tr>';
        for (const col of columns) {
            let sortIcon = '';
            if (sortCol === col) sortIcon = sortAsc ? ' ▲' : ' ▼';
            headerHtml += `<th style="cursor:pointer;" data-col="${col}">${col}${sortIcon}</th>`;
        }
        headerHtml += '</tr>';
        tableHeader.innerHTML = headerHtml;

        // Click sort
        tableHeader.querySelectorAll('th').forEach(th => {
            th.addEventListener('click', () => {
                const col = th.dataset.col;
                if (sortCol === col) sortAsc = !sortAsc;
                else { sortCol = col; sortAsc = true; }
                currentPage = 1;
                renderAll();
            });
        });

        // Body
        const paginated = getPaginatedData();
        if (paginated.length === 0) {
            tableBody.innerHTML = '<tr><td colspan="999" class="text-center text-muted">Tiada rekod sepadan.</td></tr>';
            return;
        }

        let bodyHtml = '';
        for (const row of paginated) {
            bodyHtml += '<tr>';
            for (const col of columns) {
                let val = row[col];
                if (val === null || val === undefined) val = '';
                const display = String(val);
                bodyHtml += `<td title="${display.replace(/"/g, '&quot;')}">
                    <span class="cell-preview">${display.length > 30 ? display.substring(0,30)+'…' : display}</span>
                    <span class="cell-full">${display}</span>
                </td>`;
            }
            bodyHtml += '</tr>';
        }
        tableBody.innerHTML = bodyHtml;

        // Update labels
        totalRowsLabel.textContent = fullData.length + ' baris';
        filteredRowsLabel.textContent = filteredData.length + ' ditapis';
        const tp = totalPages();
        pageInfo.textContent = `Halaman ${currentPage} / ${tp}`;
    }

    // ============================================================
    //  RENDER STATS & QUALITY
    // ============================================================
    function renderStats() {
        if (fullData.length === 0) {
            statsContainer.innerHTML = '<span class="text-muted">Tiada data.</span>';
            qualityReport.innerHTML = '<p class="text-muted">Tiada data.</p>';
            return;
        }
        const
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants