Skip to main content

How to Build a JCPC Management Analytics Dashboard?


Learn how to build a Flask-Admin management dashboard that reads booking data from SQLite, uses Pandas to analyze bookings and revenue, and presents key business metrics and charts for management.

Prerequisite:

This tutorial is part of the Flask Landing Page and Reservation System Series.

📚 View the Complete Flask Landing Page and Reservation System Series

⬅ Previous Part                                                                                                                      ➡ Next Part

Preliminary:
Before I begin, please activate the virtual environment and install the required dependencies.

python -m venv venv
venv\Scripts\activate
pip install flask, flask-admin, tinydb, matplotlib, pandas, flask-sqlalcheny
Then, we need to set up the file and folder structure as below:

I have created app.py and dashboard.html under the templates folder. Database.json is retrieved from the previous tutorial. Meanwhile, the jcpc.db will be generated automatically.

What is Flask-admin?
Flask web framework that lets developers quickly build administrative interfaces and dashboards. So, it is a simple way to create a web app with create, read, update, and delete functions, without the need to code it.

Step 1: Set up a SQLite database
The underlying logic
Extracted from app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import Mapped, mapped_column

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///jcpc.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'here is my secret key'

db = SQLAlchemy(app)

class Booking(db.Model):
    __tablename__ = 'bookings'
    id: Mapped[int] = mapped_column(primary_key=True, 
                                    autoincrement=True)
    username: Mapped[str] = mapped_column()
    court: Mapped[str] = mapped_column()
    date: Mapped[str] = mapped_column()
    time_slot: Mapped[str] = mapped_column()
    status: Mapped[str] = mapped_column()
    payment: Mapped[str] = mapped_column()
    amount: Mapped[float] = mapped_column()
    stripe_session: Mapped[str] = mapped_column()

with app.app_context():		
    db.create_all()
1. Create the Flask application
  • Creates my Flask application and tells Flask where the application is located.
2. Configure the database
  • This tells SQLAlchemy to use SQLite and name the database jcpc.db. It means SQLite is being used as the database engine.
3. Disable SQLAlchemy modification tracking
  • This disables an additional SQLAlchemy feature that tracks object modifications.
4. Configure Flask's secret key

  • Flask uses the secret key to help protect the session data.
  • For a real production application, don't hard-code your secret key in source code. Use an environment variable instead. However, for simplicity in this tutorial, I will hard-code my secret key.

5. Create the SQLAlchemy object
  • db becomes the object I use to define and interact with my database.
6. Define the Booking model
  • I'm telling SQLAlchemy, "Create a database table based on this Python class."
7. Specify the table name
  • This tells SQLAlchemy that the database table should be called "bookings."
8. Define column names
  • The id column creates an integer column called "id," and a primary key uniquely identifies each booking. While autoincrement=True means the database automatically generates the next ID.
  • The username column creates a string column.
  • The court column creates a string column.
  • The date column creates a string column.
  • The time slot column, again, is also stored as a string.
  • The status column includes pending, confirmed, cancelled, and completed in a string column.
  • The payment column includes paid, unpaid, and refunded.
  • The amount column stores the booking amount, and Python expects a floating-point number.
  • The Stripe session column stores the Stripe Checkout Session ID, and this allows me to determine which Stripe transaction belongs to which booking.
9. Create the database
  • Flask functionality needs to know the following: Which Flask application am I working with? Therefore, Flask automatically provides the context that this is the application I'm working with and creates tables if they don't exist.

Step 2: Configure the import function
Extracted from app.py
from flask_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from flask_admin.theme import Bootstrap4Theme

class MyAdminIndexView(AdminIndexView):       
    @app.route("/admin/import")
    def import_bookings():
        df = pd.read_json("database.json")
        data = df["bookings"].to_dict()
        for item in data.values():

            booking = Booking(
                username=item["username"],
                court=item["court"],
                date=item["date"],
                time_slot=item["time_slot"],
                status=item["status"],
                payment=item["payment"],
                amount=item["amount"],
                stripe_session=item["stripe_session"]
            )

            db.session.add(booking)
        db.session.commit()
        return "Bookings imported successfully"
        
class BookingView(ModelView):
    column_list = (
        "id",
        "username",
        "court",
        "date",
        "time_slot",
        "status",
        "payment",
        "amount",
        "stripe_session",
    )
    
admin.add_view(
    BookingView(
        Booking,
        db.session,
        name="Bookings"
    )
)
This function is mainly an import function that takes booking data from database.json and inserts it into my SQLAlchemy Booking table.

1. MyAdminIndexView
  • I will create my own admin index view by inheriting from Flask-Admin's AdminIndexView.
2. Create an /admin/import route.
  • This creates a normal Flask route that is responsible for importing the bookings.
3. Read the JSON file with Pandas.
  •  This reads database.json using Pandas. Therefore, Pandas reads that JSON into a DataFrame.
4. Get the bookings column
  • This is doing two things.
    • First, it selects the bookings column/series.
    • Then it converts it into a Python dictionary.
5. Loop through the bookings

  • It means going through every booking one at a time.
6. Create a SQLAlchemy Booking object

  • Now this is where the JSON data gets converted into my SQLAlchemy model according to my model in step 1 above.
7. Add and commit the booking to the database session

  • I will essentially be telling SQLAlchemy: "I want to insert this Booking object." Therefore, the object is added to SQLAlchemy's session, and the database change is committed.
8. Return a message

  • After the import finishes, the browser receives the following message: Bookings imported successfully.
9. BookingView

  • It tells Flask-Admin: "I want to create an admin interface for my Booking model," and Flask-Admin generates the following interface for me, which includes the following: 
    • viewing bookings
    • listing bookings
    • editing bookings
    • deleting bookings

10. column_list

  • This tells Flask-Admin which columns to display in the admin list. Therefore, without Flask-Admin, it may automatically determine which fields to display. 
11. Register the booking model with Flask-Admin.
  • This connects my Booking model to Flask-Admin and uses this SQLAlchemy database session.

---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Get your FREE Flask Cheat Sheet

Get more Flask, Python automation, Docker, and HTMX tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

Download my FREE Flask Cheat Sheet (PDF)


---------------------------------------------------------------------------------------------------------------------------------------------------

Step 3: Display the KPI and chart
The underlying logic
Extracted from app.py
from flask_admin import Admin, AdminIndexView, expose
from flask_admin.contrib.sqla import ModelView
from flask_admin.theme import Bootstrap4Theme
 

 class MyAdminIndexView(AdminIndexView): 
   @expose("/")
    def index(self, **kwargs): 
        bookings = db.session.scalars(select(Booking)).all()
        print(f"Bookings from database: {bookings}")
       
        df = pd.DataFrame([
            {
                "username": booking.username,
                "court": booking.court,
                "date": booking.date,
                "time_slot": booking.time_slot,
                "status": booking.status,
                "payment": booking.payment,
                "amount": booking.amount,
                "stripe_session": booking.stripe_session
            }
            for booking in bookings
        ])

        total_bookings = f"{len(df)}"

        total_revenue = df["amount"].sum()
        formatted_revenue = f"{total_revenue:.2f}"
        bookings_by_court = (
            df.groupby("court")
              .size()
              .to_dict()
        )
   
        total_customers = len(df['username'].unique())
        repeat_customers = (df["username"].value_counts() > 1).sum()
       
        chart_df = pd.DataFrame(
            list(bookings_by_court.items()),
            columns=["court", "bookings"]
        )
        
        fig = chart_df.plot.bar(
            x = 'court',
            y = "bookings",
            title = "Bookings by Court",
            grid= True,
            rot = 45,
            mark_right=True,
            figsize=(7,3)
        ).get_figure()
        
        img = io.BytesIO()
        fig.savefig(img, format="png", bbox_inches="tight")
        img.seek(0)
        
        chart = base64.b64encode(img.getvalue()).decode("utf-8")
        
        return self.render(
            "dashboard.html",
            total_bookings=total_bookings,
            total_revenue=formatted_revenue,
            total_customers=total_customers,
            repeat_customers=repeat_customers,
            bookings_by_court=bookings_by_court,
            chart=chart
        )
        
admin = Admin(app, 
              name="JCPC Admin", 
              theme=Bootstrap4Theme(swatch='slate'),
              index_view=MyAdminIndexView())
1. @expose("/")
  • It tells Flask-Admin that when the user visits the main page of this admin view, execute index(). So if my admin URL is http://127.0.0.1:5000/admin/,
2. Retrieve bookings from SQLAlchemy

  • It means selecting all records from the Booking model, and SQLAlchemy returns the actual Booking objects rather than rows containing extra structure.
3. Convert SQLAlchemy objects into Pandas.

  • This is a list comprehension. It means that I will be taking every SQLAlchemy Booking object and converting it into a Python dictionary. An example below.

SQLAlchemy Booking object

Booking(
    username="Kelvin",
    court="St George",
    amount=20
)
Python dictionary
{
    "username": "Kelvin",
    "court": "St George",
    "amount": 20
}

4. Calculate total bookings
  • len(df) gives me the number of rows. For example: 10 bookings. Then, it will convert it into a string: '10'.
5. Calculate total revenue.
  • This selects the amount column, df["amount"], and calculates the total: .sum().
6. Format the revenue

  • .2f means "display the number with exactly two decimal places."
7. Calculate bookings by court

  • It groups the rows by court. An example: Court: St. George, and the users: Kelvin, John, and Sarah.
  • Then, .size() will count how many bookings are in each group: St. George: 3.
  • Finally, .to_dict() converts it to {"St George": 3}.
8. Calculate total customers

  • This counts unique usernames. There are five bookings, but only three unique customers: Kelvin, John, and Mary. So, it produces something like ["Kelvin", "John", "Mary"] and gives 3 unique customers.

9. Calculate repeat customers
  • First, count how many bookings each customer has. For example:
Kelvin    3
John      1
Mary      2
  • Then, it checks which customers have more than one booking. Finally, Kelvin and Mary meet the criteria. So, the repeat customer count is 2.
10. Create a DataFrame for the chart
  • First: The "bookings_by_court.items()"" function will generate as follows:
("St George", 3)
("St Louis", 1)
("St Mark", 1)
  • However, list(...) converts them into a list:
[
    ("St George", 3),
    ("St Louis", 1),
    ("St Mark", 1)
]
  • Now I have a DataFrame specifically prepared for my chart.
11. Create the bar chart
  • I will be using Pandas' plotting functionality, which internally uses Matplotlib.
    • plot.bar()—Creates a bar chart
    • x='court'—represents the court
    • y="bookings"—represents the booking numbers
    • title—sets the chart title.
    • grid=True—Displays grid lines.
    • rot=45—Rotates the X-axis labels by 45 degrees.
    • figsize=(7,3) - 7 inches wide and 3 inches high
    • .get_figure() - The Pandas plotting function returns an Axes object.
12. Create an in-memory image
  • Instead of saving the chart to a physical file like chart.png. I will create it as an in-memory binary file, and this is very useful for web applications.
13. Save the chart into memory
  • Save this chart as a 'PNG' into img. So, no chart.png needs to be created on my server.
14. Move back to the beginning of the image
  • seek(0) moves the pointer back to the beginning. This is necessary before reading the image data.
15. Convert the image to Base64
  • Now convert the binary image into Base64 text, as HTML can embed a Base64 image directly. Then, it converts the Base64 bytes into a normal Python string.
16. Send everything to the template
  • This renders dashboard.html and passes all my calculated values into the template.
17. Register the admin dashboard with Flask-Admin.
  • Create a Flask-Admin interface for my Flask application, call it JCPC Admin, use this Bootstrap theme, and use my custom dashboard as the home page.
  • Bootstrap4Theme tells Flask-Admin to use a Bootstrap 4-based theme and selects the Slate Bootswatch style. This controls things such as the colours and general styling of the admin interface.

Dashboard Interface

templates/dashboard.html
<{% extends 'admin/master.html' %}>

<{% block body %}>

<div class="container">

    <h2>JCPC Management Dashboard</h2>

    <div class="row">

        <!-- Left Column: Summary Cards + Chart -->
        <div class="col-md-8">
            <div class="row">
                <div class="col-md-6">
                    <div class="card">
                        <div class="card-body">
                            <h5>Total Bookings</h5>
                            <h2>{{ total_bookings }}</h2>
                        </div>
                    </div>
                </div>

                <div class="col-md-6">
                    <div class="card">
                        <div class="card-body">
                            <h5>Total Revenue</h5>
                            <h2>${{ total_revenue }}</h2>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Chart inserted directly below Total Bookings and Total Revenue -->
            <div class="row mt-4">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-body">
                            <h5>Bookings by Court Chart</h5>
                            <img
                                src="data:image/png;base64,{{ chart }}"
                                class="img-fluid"
                                alt="Bookings by Court"
                            >
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Right Column: Text Breakdown & Total Visitors -->
        <div class="col-md-4">
            <div class="card">
                <div class="card-body">
                    <h5>Bookings by Court</h5>
                    {% for court, count in bookings_by_court.items() %}
                        <p style="font-size: x-large;">{{ court }}: {{ count }}</p>
                    {% endfor %}
                </div>
            </div>

            <!-- Total customers placed directly under Bookings by Court -->
            <div class="card mt-4">
                <div class="card-body">
                    <h5>Total Customers</h5>
                    <h2>{{ total_customers }}</h2>
                </div>
            </div>

            <!-- Repeat Customers placed directly under Total Visitors -->
            <div class="card mt-4">
                <div class="card-body">
                    <h5>Repeat Customers</h5>
                    <h2>{{ repeat_customers }}</h2>
                </div>
            </div>
        </div>
    </div>
</div>

<{% endblock %}>
1. Extending Flask-Admin's template
  • This tells Jinja: "Start with Flask-Admin's standard admin/master.html template." Instead of creating a complete HTML document myself, I'm reusing Flask-Admin's layout. This is because Flask-Admin's master.html already handles that.
2. Replacing the body section

  • My template says, "Put my dashboard content inside the body block," and at the bottom, it closes that block. So, my dashboard becomes the main content of the Flask-Admin page.
3. Bootstrap container
  • This is a Bootstrap class. It creates a centred container with appropriate horizontal spacing.
4. Dashboard title
  • This simply displays the JCPC Management Dashboard, and <h2> is an HTML heading.
5. Bootstrap row
  • Bootstrap uses a 12-column grid system. Therefore, it indicates that the left side takes approximately 2/3 of the screen and the right side takes approximately 1/3.
6. Left column

  • This contains:

    1. Total Bookings
    2. Total Revenue
    3. Bookings by Court chart
7. Another row for the summary cards.
  • Inside the left column, I create another Bootstrap row. Then, I have two <"col-md-6">, and therefore I will get two cards side by side.
8. Total Bookings card
  • Both "card" and "card-body" are Bootstrap classes. This means it displays the value from this Python variable.
  • For example, if my Flask code shows total_bookings = 25. So the browser will display 25.
9. Total Revenue
  • Suppose Python calculates the following: total_revenue = 1250.50. So, Jinja will render $1250.50, and {{ total_revenue }} is simply the dynamic Python value.
10. The chart
  • This is one of the more interesting parts. My Python code probably creates a Matplotlib chart and converts it into Base64.
  • Then, the template does this: "data: image/png;base64...." So the browser receives the image directly inside the HTML.
  • That means I don't need to save the "chart.png" to the static folder.
11. img-fluid
  • This is Bootstrap; it makes the image responsive. In simple terms: Don't let the chart overflow its container. So if the dashboard becomes narrower, the chart can shrink accordingly.
12. Right column

  • This is the right-hand side of my dashboard.
  • It contains:

    1. Bookings by Court
    2. Total Customers
    3. Repeat Customers
13. Bookings by Court
  • This is the important Jinja part: "{% for court, count in bookings_by_court.items() %}" Here I am looping through a Python dictionary.
bookings_by_court = {
    "st_george": 10,
    "st_louis": 7,
    "st_mark": 8
}
  • While it .items() will give me the pairs:
court          count
--------------------
st_george      10
st_louis        7
st_mark         8
  • Therefore, it means the following: for every key/value pair in the dictionary, put the key into count and the value into count.
14. {% %} versus {{ }}
  • It means {{ }} displaying something
  • While {% %} perform template logic
15. mt-4
  • mt-4 is another Bootstrap utility class. "mt" means "margin-top," which means adding some space above this element. That's why my cards don't appear directly against each other.
16. Total Customers
  • If Python has total_customers = 15, the template will display 15
17. Repeat Customers
  • If Python has repeat_customers = 5, the template will also display 5

Final wrap-up

My Flask-Admin dashboard combines Flask, Jinja2, Bootstrap, Pandas, and Matplotlib to turn my booking data into a simple management dashboard. Python/Pandas calculates metrics such as total bookings, revenue, customers, repeat customers, and bookings by court, while Matplotlib creates the chart. Flask passes these results to the Jinja2 template, which {{ }} displays the values and {% for %} loops through the court data. Bootstrap then organises everything into cards, columns, and responsive sections, giving me a complete dashboard for monitoring my JCPC booking system.

Published: August 2026
Last Updated: August 2026

---------------------------------------------------------------------------------------------------------------------------------------------------

Thanks for reading! 

If you haven't subscribed yet, join my newsletter to receive future Python and Flask tutorials.


---------------------------------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 

---------------------------------------------------------------------------------------------------------------------------------------------------

About the Author 
Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.