Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SQL Dashboard Builder

Tests PHP Version License

Turn a handful of SQL queries into one self-contained HTML file.

No CDN. No <script>. No build step. No dashboard server to keep alive. Just a file you can email to a director, attach to a ticket, or archive for an auditor — and it will still open correctly on a laptop with no network in five years.

Why this exists

Reporting tools are excellent until the report has to leave the building.

A BI dashboard behind SSO cannot be emailed. A React dashboard needs a build and a host. A CSV export loses every bit of framing that made the numbers mean something. And a chart library loaded from a CDN quietly becomes a blank rectangle the day that CDN is blocked by a corporate proxy — which is exactly the day the client opens it.

So: run the queries, inline everything, hand over one file.

┌── deals.sql ──┐
│  KPI          │      Dashboard::make($pdo, 'Sales')
│  trend        │  ──▶     ->add(Kpi …)          ──▶   sales.html   (8 KB, opens anywhere)
│  breakdown    │          ->add(LineChart …)
└───────────────┘          ->writeTo('sales.html')

Install

composer require neeraj-patel/sql-dashboard-builder

Quick start

use NeerajPatel\SqlDashboard\Dashboard;
use NeerajPatel\SqlDashboard\Widgets\{Kpi, Table, BarChart, LineChart};

$pdo = new PDO('mysql:host=localhost;dbname=crm', $user, $pass);

Dashboard::make($pdo, 'Sales Performance')
    ->subtitle('All regions · financial year to date')

    ->add(Kpi::of('Open pipeline', "SELECT SUM(amount) AS value FROM deals WHERE status='open'")
            ->format('crore'))

    ->add(Kpi::of('Won this month', "
              SELECT SUM(CASE WHEN month = :now  THEN amount END) AS value,
                     SUM(CASE WHEN month = :prev THEN amount END) AS previous
              FROM deals WHERE status = 'won'
          ", ['now' => '2026-09', 'prev' => '2026-08'])
            ->format('crore')
            ->accent('#0ca30c'))

    ->add(LineChart::of('Won value by month', "
              SELECT month AS label, SUM(amount) AS value
              FROM deals WHERE status='won' GROUP BY month ORDER BY month
          ")->format('crore'))

    ->add(Table::of('Product performance', "
              SELECT product, COUNT(*) AS deals, SUM(amount) AS value
              FROM deals GROUP BY product ORDER BY value DESC
          ")
            ->formats(['deals' => 'number', 'value' => 'crore'])
            ->bars(['value']))

    ->writeTo(__DIR__ . '/reports/sales.html');

Run the bundled example to see the output without wiring up a database:

composer install
php examples/sales-dashboard.php
# Wrote examples/output/sales.html (8.2 KB)

Widgets

Widget Expects Notes
Kpi one row with value, optionally previous renders the headline number and an up/down delta
Table any result set column headings, number formats, in-cell data bars
BarChart label, value horizontal bars as inline SVG
LineChart label, value, ordered oldest first trend line with optional area fill

Every widget takes bindings, which are passed straight to a prepared statement:

Kpi::of('Won in region', 'SELECT SUM(amount) AS value FROM deals WHERE region = :region',
        ['region' => $region]);

Formats

number · inr (Indian grouping — 12,34,567) · crore (12.35 Cr) · compact (auto Cr / L / K) · percent

Layout

Widgets drop into a 12-column grid. Kpi takes 3 columns, everything else defaults to 6 and accepts ->width('third' | 'half' | 'full'). Below 900px it collapses to a single column, and there is a print stylesheet so a widget never splits across a page.

Design decisions

Everything inlined. The promise is one portable file, so there is no chart library and no web font — charts are hand-drawn SVG and the CSS ships in a <style> block. The example dashboard with four KPIs, two charts and two tables is 8 KB.

Escape at the boundary. Report data is customer-entered text: company names with &, addresses with <. Every value goes through htmlspecialchars on its way into the page, and there is a test that proves a <script> tag stored in the database comes out inert.

Queries run in sequence. A report build is not the place to hold a dozen connections open. This is designed to run from a small cron slot, so it walks the widgets one at a time.

Dark mode by media query, not by toggle. No JavaScript means no toggle — the page follows the reader's system setting through prefers-color-scheme.

CLI

export SQLDASH_DSN="mysql:host=localhost;dbname=crm"
export SQLDASH_USER=reporting
export SQLDASH_PASS=secret

vendor/bin/sqldash reports/sales.php public/sales.html

Where reports/sales.php returns a callable:

<?php
return fn (PDO $pdo) => Dashboard::make($pdo, 'Sales')->add(/* … */);

Pair it with a nightly cron entry and the report rebuilds itself.

Testing

composer test

14 tests, 32 assertions — covering rendering, deltas, data bars, empty result sets, escaping, number formatting and file output.

Requirements

PHP 8.1+ with PDO. Works with any PDO driver — the examples and tests use SQLite, and it is used against MySQL in anger.

License

MIT — see LICENSE.

About

Turn SQL queries into one self-contained HTML dashboard - no CDN, no script tags, no server.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages