From 1d94c3ef51672cb66418ee260895ad5b251a1961 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 2 Oct 2025 19:19:12 +0000 Subject: [PATCH] initial csv uploader and dashboard --- assets/css/custom.css | 115 +++++++++++++++++ assets/js/main.js | 203 ++++++++++++++++++++++++++++++ dashboard.php | 225 +++++++++++++++++++++++++++++++++ index.php | 282 +++++++++++++++++++++--------------------- upload.php | 127 +++++++++++++++++++ 5 files changed, 810 insertions(+), 142 deletions(-) create mode 100644 assets/css/custom.css create mode 100644 assets/js/main.js create mode 100644 dashboard.php create mode 100644 upload.php diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..9d4d11b --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,115 @@ +/* +Palette: +- Base: #FFFFFF +- Panels: #F7F3ED +- Hovers: #E8E1D9 +- Accent: #D4A373 +- Text: #333333 +*/ + +body { + background-color: #FFFFFF; + font-family: 'Inter', sans-serif; + color: #333333; + padding-top: 2rem; + padding-bottom: 2rem; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; + font-weight: 700; +} + +h1 { font-size: 32px; } +h2 { font-size: 24px; } +h3 { font-size: 20px; } + +.container { + max-width: 1200px; +} + +.card { + background-color: rgba(247, 243, 237, 0.8); + backdrop-filter: blur(4px); + border: 1px solid rgba(0,0,0,0.05); + border-radius: 14px; + box-shadow: 0 8px 16px rgba(0,0,0,0.04); + padding: 1.5rem; +} + +.card-title { + margin-bottom: 1.5rem; +} + +.btn { + border-radius: 50px; + padding: 12px 24px; + font-weight: 500; + transition: all 0.2s ease-in-out; +} + +.btn-primary { + background-color: #D4A373; + border-color: #D4A373; + color: #fff; +} + +.btn-primary:hover { + background-color: #C39263; + border-color: #C39263; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.btn-secondary { + background-color: #fff; + border-color: #E8E1D9; + color: #333; +} + +.btn-secondary:hover { + background-color: #E8E1D9; + border-color: #E8E1D9; +} + +.form-control, .form-select { + border-radius: 10px; + background-color: #FFFFFF; + border: 1px solid #E8E1D9; + padding: 12px; +} + +.form-control:focus, .form-select:focus { + border-color: #D4A373; + box-shadow: 0 0 0 4px rgba(212, 163, 115, 0.15); +} + +.table { + border-color: #E8E1D9; +} + +.table th { + font-weight: 500; + color: #777; +} + +.table-hover tbody tr:hover { + background-color: rgba(232, 225, 217, 0.5); +} + +.lead { + color: #666; + font-size: 1.1rem; +} + +.divider { + border-top: 1px solid rgba(0,0,0,0.07); + margin: 2rem 0; +} + +/* Chart.js minimal styling */ +.chart-container { + position: relative; + height: 40vh; + width: 100%; +} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..4f32a0c --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,203 @@ +// Main javascript file + +document.addEventListener('DOMContentLoaded', function () { + const filters = document.querySelectorAll('select[id^="filter_"]'); + filters.forEach(filter => { + filter.addEventListener('change', applyFilters); + }); + + if (document.getElementById('dateRange')) { + const dateRangePicker = $('#dateRange').daterangepicker({ + opens: 'left', + autoUpdateInput: false, + locale: { + cancelLabel: 'Clear' + } + }); + + dateRangePicker.on('apply.daterangepicker', function(ev, picker) { + $(this).val(picker.startDate.format('YYYY-MM-DD') + ' - ' + picker.endDate.format('YYYY-MM-DD')); + applyFilters(); + }); + + dateRangePicker.on('cancel.daterangepicker', function(ev, picker) { + $(this).val(''); + applyFilters(); + }); + } + + // Keep track of chart instances + const charts = {}; + + function applyFilters() { + const activeFilters = {}; + filters.forEach(f => { + if (f.value) { + activeFilters[f.id.replace('filter_', '')] = f.value; + } + }); + + let filteredData = sampleData.filter(row => { + for (const col in activeFilters) { + if (row[col] != activeFilters[col]) { + return false; + } + } + return true; + }); + + const dateRange = $('#dateRange').val(); + if (dateRange && charts.timeChart) { + const dates = dateRange.split(' - '); + const startDate = moment(dates[0], 'YYYY-MM-DD'); + const endDate = moment(dates[1], 'YYYY-MM-DD'); + const timeCol = Object.keys(charts.timeChart.data.datasets[0]._meta)[0]; + + filteredData = filteredData.filter(row => { + const rowDate = moment(row[timeCol], 'YYYY-MM-DD'); + return rowDate.isBetween(startDate, endDate, null, '[]'); + }); + } + + updateDashboard(filteredData); + } + + function updateDashboard(data) { + updateKpis(data); + updateCharts(data); + updateTable(data); + } + + function updateKpis(data) { + // This is a simplified example. It assumes the first KPI is always the one to be updated. + const kpiElements = document.querySelectorAll('.card-text.fs-4'); + if (kpiElements.length >= 3) { + kpiElements[0].textContent = data.length; + + const kpiCol = document.querySelector('.card-subtitle.mb-2.text-muted').textContent.replace('Avg. ', '').replace('Total ', ''); + + const total = data.reduce((sum, row) => sum + parseFloat(row[kpiCol] || 0), 0); + const avg = data.length > 0 ? total / data.length : 0; + + kpiElements[1].textContent = Math.round(avg * 100) / 100; + kpiElements[2].textContent = total; + } + } + + function updateCharts(data) { + if (charts.timeChart) { + const timeCol = charts.timeChart.data.datasets[0].label.replace(' over Time', ''); + charts.timeChart.data.labels = data.map(row => row[timeCol]); + charts.timeChart.data.datasets[0].data = data.map(row => row[timeCol]); + charts.timeChart.update(); + } + + if (charts.categoryChart) { + const catCol = charts.categoryChart.data.datasets[0].label.replace('Top 10 by ', ''); + const metCol = charts.categoryChart.data.datasets[0].label.replace('Top 10 by ', ''); + + const grouped_data = {}; + data.forEach(row => { + const category = row[catCol]; + if (!grouped_data[category]) { + grouped_data[category] = 0; + } + grouped_data[category] += parseFloat(row[metCol]); + }); + + const sorted_data = Object.entries(grouped_data).sort(([,a],[,b]) => b-a).slice(0, 10); + + charts.categoryChart.data.labels = sorted_data.map(item => item[0]); + charts.categoryChart.data.datasets[0].data = sorted_data.map(item => item[1]); + charts.categoryChart.update(); + } + + if (charts.histogramChart) { + const metCol = charts.histogramChart.data.datasets[0].label.replace('Histogram of ', ''); + charts.histogramChart.data.labels = data.map(row => row[metCol]); + charts.histogramChart.data.datasets[0].data = data.map(row => row[metCol]); + charts.histogramChart.update(); + } + } + + function updateTable(data) { + const tableBody = document.querySelector('.table-responsive tbody'); + tableBody.innerHTML = ''; + data.forEach(row => { + const tr = document.createElement('tr'); + for (const cell in row) { + const td = document.createElement('td'); + td.textContent = row[cell]; + tr.appendChild(td); + } + tableBody.appendChild(tr); + }); + } + + document.getElementById('exportCsv').addEventListener('click', exportToCsv); + + function exportToCsv() { + const activeFilters = {}; + filters.forEach(f => { + if (f.value) { + activeFilters[f.id.replace('filter_', '')] = f.value; + } + }); + + let filteredData = sampleData.filter(row => { + for (const col in activeFilters) { + if (row[col] != activeFilters[col]) { + return false; + } + } + return true; + }); + + const dateRange = $('#dateRange').val(); + if (dateRange && charts.timeChart) { + const dates = dateRange.split(' - '); + const startDate = moment(dates[0], 'YYYY-MM-DD'); + const endDate = moment(dates[1], 'YYYY-MM-DD'); + const timeCol = Object.keys(charts.timeChart.data.datasets[0]._meta)[0]; + + filteredData = filteredData.filter(row => { + const rowDate = moment(row[timeCol], 'YYYY-MM-DD'); + return rowDate.isBetween(startDate, endDate, null, '[]'); + }); + } + + if (filteredData.length === 0) { + alert("No data to export."); + return; + } + + const headers = Object.keys(filteredData[0]); + const csvContent = [ + headers.join(','), + ...filteredData.map(row => headers.map(header => JSON.stringify(row[header])).join(',')) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', 'filtered_data.csv'); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + document.querySelectorAll('.export-png').forEach(button => { + button.addEventListener('click', function() { + const chartId = this.dataset.chart; + const canvas = document.getElementById(chartId); + html2canvas(canvas.parentElement).then(canvas => { + const link = document.createElement('a'); + link.download = chartId + '.png'; + link.href = canvas.toDataURL(); + link.click(); + }); + }); + }); +} \ No newline at end of file diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..dacee36 --- /dev/null +++ b/dashboard.php @@ -0,0 +1,225 @@ +No data available to generate a dashboard.

'; + return; +} + +$analysis = $_SESSION['csv_analysis']; +$schema = $analysis['schema']; +$suggestions = $analysis['suggestions']; +$sample = $analysis['sample']; + +// Apply user-defined types from preview screen +if (isset($_POST['column_types'])) { + foreach ($_POST['column_types'] as $col => $type) { + if (isset($schema[$col])) { + $schema[$col]['type'] = $type; + } + } + // Re-run suggestions with updated types + // (This requires the suggestion logic to be available here) + // For now, we'll just use the updated schema +} + +?> + +
+ +
+
+
+
Filters
+
+ +
+ + +
+ + +
+ + +
+ +
+
+
+
+ + +
+
+ +
+
+
Total Rows
+

+
+
+
+
+
Avg.
+

+
+
+
+
+
Total
+

+
+
+ +
+
+ + +
+
+ +
+
+
Time Chart
+ +
+ +
+ +
+
+
Histogram
+ +
+ +
+ +
+
+ +
+
+
Category Chart
+ +
+ +
+ +
+
Top Categories
+ + +
+
+ +
+
+ + +
+
+
+
+
Sample Data
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + diff --git a/index.php b/index.php index 7205f3d..1755a07 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,148 @@ - $type) { + if (isset($_SESSION['csv_analysis']['schema'][$col])) { + $_SESSION['csv_analysis']['schema'][$col]['type'] = $type; + } + } + // Re-run suggestions if needed (or just use updated schema in dashboard.php) + } + $_SESSION['show_dashboard'] = true; + } + header('Location: index.php'); + exit; +} + +$show_dashboard = isset($_SESSION['show_dashboard']) && $_SESSION['show_dashboard']; +$csv_analysis = isset($_SESSION['csv_analysis']) ? $_SESSION['csv_analysis'] : null; -$phpVersion = PHP_VERSION; -$now = date('Y-m-d H:i:s'); ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + dashboard-for-philip-v001 + + + + + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+

Instant CSV Dashboard

+

Upload a CSV or TSV file to automatically generate an interactive dashboard.

+
+
+ + +
+ +
+ + + +
+
+
+

Schema Preview

+ +
+ Upload New File + +
+ + + + + + + + + + + $info): ?> + + + + + + + + +
Column NameGuessed TypeUnique ValuesBlank Values
+ +
+
+ Upload New File + +
+
+ +
+
+
+ +
+
+
+
+

Upload Your Data

+
+
+ +
+
+ +
+
+
+
+
+
+ +
-
- + + + + + + + - + \ No newline at end of file diff --git a/upload.php b/upload.php new file mode 100644 index 0000000..51cff5b --- /dev/null +++ b/upload.php @@ -0,0 +1,127 @@ + 'Could not open file']; + } + + // 1. Detect Delimiter + $firstLine = fgets($fileHandle); + $commaCount = substr_count($firstLine, ','); + $tabCount = substr_count($firstLine, "\t"); + $delimiter = $tabCount > $commaCount ? "\t" : ','; + + // Reset pointer and read header + rewind($fileHandle); + $header = fgetcsv($fileHandle, 0, $delimiter); + + $data = []; + $rowCount = 0; + while (($row = fgetcsv($fileHandle, 0, $delimiter)) !== false && $rowCount < $rowsToAnalyze) { + if (count($row) == count($header)) { + $data[] = array_combine($header, $row); + } + $rowCount++; + } + fclose($fileHandle); + + if (empty($data)) { + return ['error' => 'No data found in file or header mismatch.']; + } + + // 2. Guess Column Types and Stats + $schema = []; + foreach ($header as $col) { + $values = array_column($data, $col); + $uniqueVals = array_unique($values); + $blankCount = count(array_filter($values, fn($v) => $v === '' || $v === null)); + + // Type detection + $type = 'text'; + $isNumeric = true; + $isInteger = true; + $isDate = true; + + foreach ($values as $val) { + if ($val === '' || $val === null) continue; + + if (!is_numeric($val)) $isNumeric = false; + if (filter_var($val, FILTER_VALIDATE_INT) === false) $isInteger = false; + if (strtotime($val) === false) $isDate = false; + } + + if ($isDate) { + $type = 'date'; + } elseif ($isNumeric) { + $type = $isInteger ? 'integer' : 'decimal'; + } + + $schema[$col] = [ + 'type' => $type, + 'unique_count' => count($uniqueVals), + 'blank_count' => $blankCount + ]; + } + + // 3. Suggest dashboard items + $suggestions = suggestDashboard($schema); + + return [ + 'delimiter' => $delimiter, + 'header' => $header, + 'schema' => $schema, + 'suggestions' => $suggestions, + 'sample' => array_slice($data, 0, 20) + ]; +} + +function suggestDashboard($schema) { + $suggestions = []; + $dateCols = []; + $numericCols = []; + $categoryCols = []; + + foreach ($schema as $col => $info) { + if ($info['type'] === 'date') $dateCols[] = $col; + if ($info['type'] === 'integer' || $info['type'] === 'decimal') $numericCols[] = $col; + if ($info['type'] === 'text' && $info['unique_count'] < 50) $categoryCols[] = $col; + } + + if (!empty($dateCols) && !empty($numericCols)) { + $suggestions['time_chart'] = ['time_col' => $dateCols[0], 'metric_col' => $numericCols[0]]; + } elseif (!empty($numericCols)) { + $suggestions['histogram'] = ['metric_col' => $numericCols[0]]; + } + + if (!empty($categoryCols) && !empty($numericCols)) { + $suggestions['category_chart'] = ['category_col' => $categoryCols[0], 'metric_col' => $numericCols[0]]; + } elseif (empty($numericCols) && !empty($categoryCols)) { + $suggestions['category_table'] = ['category_col' => $categoryCols[0]]; + } + + $suggestions['kpis'] = !empty($numericCols) ? $numericCols : []; + $suggestions['filters'] = array_slice($categoryCols, 0, 2); + + return $suggestions; +} \ No newline at end of file