Build an Analytics Dashboard

Add a role-enforced revenue analytics dashboard to a hospital system.

Introduction

30 Second Summary

Every hospital needs to understand where its revenue comes from. Without clear data, administrators make decisions in the dark and doctors have no visibility into their own performance.

In this project, you will build a role-enforced analytics dashboard for the Korutla Hospital system that visualizes billing revenue over time with interactive charts and enforces access control so each role sees only the data they are authorized to view.

What You'll Build

A React analytics page powered by an ASP.NET Core API endpoint that aggregates billing data by period and enforces role-based access control, visualized with Recharts bar charts.

By the end of this project, you'll have:

  • A CreatedById field on the Bill model linking each bill to the user who created it, backed by a database migration.
  • An AnalyticsController endpoint that aggregates revenue by day, month, or year using LINQ GroupBy, with role enforcement ensuring Admins see all data and Doctors see only their own.
  • A React dashboard page with a Recharts bar chart, period selector, and conditional doctor breakdown table, integrated with the existing RBAC routing and navigation.
  • Secret Mission: Add a top-performing doctors leaderboard ranked by total revenue, visible only to Admins.

Are there any prerequisites?

This project builds on the Korutla Hospital system with role-based access control already in place. You need the completed RBAC project running locally with both the API and React frontend functional.

You also need VS Code or Cursor, the .NET SDK, Node.js, and PostgreSQL installed on your machine.

Install Recharts and Link Bills to Doctors

Before you can build an analytics dashboard, you need two things: a charting library to render visualizations, and a data trail linking each bill to the user who created it.

In this step, you will install Recharts for charting and add a CreatedById field to the Bill model so the analytics endpoint can group revenue by doctor.

In this step, get ready to:
  • Install the Recharts charting library in the React frontend.
  • Add a CreatedById field to the Bill model that links each bill to the logged-in user.
  • Run a migration and update the bill creation endpoint to set CreatedById automatically from the JWT claims.
Install Recharts

Recharts is a React-native charting library with a declarative JSX API. It ships its own TypeScript types and integrates seamlessly with the existing hospital frontend.

  • In your terminal, navigate to the hospital-frontend directory and install Recharts along with its required peer dependency by running this command:
npm install recharts@3.9.0 react-is

What does this command do?

This installs two packages. recharts@3.9.0 is the charting library pinned to a stable version. react-is is a peer dependency that Recharts 3.x requires to detect React element types internally.

  • Open package.json in the hospital-frontend folder and confirm you see recharts and react-is listed under dependencies.

Install failed or missing packages?

Make sure you are running the command from inside the hospital-frontend directory, not the root project folder or the API directory.

If you see a peer dependency warning, that is usually safe to ignore as long as recharts appears in package.json after the install completes.

help me troubleshoot my Recharts installation error

Add CreatedById to the Bill model and create a migration

The analytics endpoint needs to know which user created each bill. You will add a foreign key property that links each Bill record to an IdentityUser entry.

  • Switch back to the HospitalApi project and open the Bill model file (HospitalApi/Models/Bill.cs).
  • Add the following two properties inside the Bill class, below the existing properties:
public string? CreatedById { get; set; }
public IdentityUser? CreatedBy { get; set; }

What do these properties do?

CreatedById stores the ID of the user who created the bill. It is nullable (string?) because existing bills in the database predate this field and will have null values.

CreatedBy is the navigation property that lets Entity Framework load the related IdentityUser record when needed.

  • Make sure you also have using Microsoft.AspNetCore.Identity; at the top of the file so the IdentityUser type resolves.
  • Save Bill.cs.

Next, you need to tell Entity Framework how this relationship works.

  • Open HospitalApi/Data/HospitalDbContext.cs.
  • Inside the OnModelCreating method, add the following relationship configuration:
modelBuilder.Entity<Bill>()
    .HasOne(b => b.CreatedBy)
    .WithMany()
    .HasForeignKey(b => b.CreatedById)
    .OnDelete(DeleteBehavior.SetNull);

What does this configuration do?

This tells EF Core that each Bill has one related IdentityUser (the creator). The .WithMany() call means a user can create many bills. The foreign key is CreatedById.

DeleteBehavior.SetNull means if a user account is deleted, the bill stays in the database but its CreatedById is set to null. This prevents losing billing records when staff accounts are removed.

  • Save HospitalDbContext.cs.

Now create and apply the database migration to add the new column to your PostgreSQL database.

  • In your terminal, navigate to the HospitalApi directory and run these commands:
dotnet ef migrations add AddBillCreatedBy
dotnet ef database update

What do these commands do?

The first command generates a migration file that describes the schema change (adding a CreatedById column to the Bills table). The second command applies that migration to your running PostgreSQL database.

Both commands should finish with "Done" in the terminal output.

  • Confirm both commands completed successfully. You should see "Done" printed at the end of each command's output.

Migration command failed?

Make sure you are running from the HospitalApi directory, not the frontend or root folder.

If you see a build error, check that Bill.cs compiles cleanly. A missing using Microsoft.AspNetCore.Identity; statement is the most common cause.

If the database update fails with a connection error, confirm PostgreSQL is running locally.

help me fix my EF Core migration error for the AddBillCreatedBy migration

Update the bill creation endpoint to capture the current user

The migration adds the column, but existing code does not populate it yet. You need to set CreatedById from the logged-in user's JWT claims every time a bill is created.

  • Open the controller that creates bills (HospitalApi/Controllers/BillsController.cs or your equivalent billing controller).
  • At the top of the file, add this using statement if it is not already present:
using System.Security.Claims;
  • Find the action method that creates a new bill (the one that saves a Bill object to the database).
  • Add this line before the bill is saved to the database:
// Link this bill to the logged-in user
bill.CreatedById = User.FindFirstValue(ClaimTypes.NameIdentifier);

What does this line do?

User.FindFirstValue(ClaimTypes.NameIdentifier) extracts the current user's unique ID from their JWT token. Every authenticated request carries this claim automatically. By setting it on the bill before saving, you create the link between the bill and its creator.

  • Save BillsController.cs.
  • Restart the API by stopping and re-running it.
  • Log in through the frontend and create a test bill.
  • Check the database to confirm the CreatedById column is populated with the logged-in user's ID.

Quick database check

You can verify the column directly with a quick psql query:

psql -d HospitalDb -c "SELECT \"Id\", \"CreatedById\" FROM \"Bills\" ORDER BY \"Id\" DESC LIMIT 5;"

The newest bill should show a GUID in the CreatedById column. Older bills will show null, which is expected.

CreatedById still null on new bills?

Make sure the line bill.CreatedById = User.FindFirstValue(ClaimTypes.NameIdentifier); appears before the call that saves the bill to the database. If it comes after the save, the value is not persisted.

Confirm you restarted the API after making the change. Code changes are not picked up until the server restarts.

help me figure out why CreatedById is still null after creating a bill

Your data layer is wired up. Every new bill now carries a reference to the user who created it. Next up, you will build the analytics API endpoint that aggregates this billing data into revenue reports.

Build the Analytics API with Role Enforcement

Your Bill model now tracks who created each bill. The next step is to put that data to work by building an API endpoint that aggregates revenue over time.

The analytics endpoint needs to respect the hospital's role system. Admins should see revenue from all doctors, while Doctors should only see their own numbers. You'll use LINQ GroupBy to aggregate billing data by day, month, or year, and ASP.NET Core's Authorize attribute to enforce role-based access.

In this step, get ready to:
  • Create DTOs for the analytics response shape.
  • Build an AnalyticsController with a revenue endpoint that groups billing data by period.
  • Enforce RBAC so Admins see all revenue and Doctors see only their own.
Create the analytics DTOs

Before building the controller, you need to define the shape of the data it returns. DTOs (Data Transfer Objects) are simple classes that describe what the API response looks like.

  • Create a new file called RevenueDataPointDto.cs in HospitalApi/DTOs/ and paste the following code:
namespace HospitalApi.DTOs;

// Represents one data point on the revenue chart
public class RevenueDataPointDto
{
    public string Period { get; set; } = string.Empty;
    public decimal Revenue { get; set; }
}
  • Create a new file called DoctorRevenueDto.cs in the same HospitalApi/DTOs/ folder and paste:
namespace HospitalApi.DTOs;

// Represents a single doctor's revenue summary
public class DoctorRevenueDto
{
    public string DoctorEmail { get; set; } = string.Empty;
    public decimal TotalRevenue { get; set; }
    public int BillCount { get; set; }
}
  • Create a new file called AnalyticsResponseDto.cs in the same HospitalApi/DTOs/ folder and paste:
namespace HospitalApi.DTOs;

// The full analytics response combining totals, time-series data, and optional doctor breakdown
public class AnalyticsResponseDto
{
    public decimal TotalRevenue { get; set; }
    public int BillCount { get; set; }
    public List<RevenueDataPointDto> RevenueOverTime { get; set; } = new();
    public List<DoctorRevenueDto>? DoctorBreakdown { get; set; }
}

What do these DTOs represent?

  • RevenueDataPointDto holds one point on the revenue chart. Each point has a time period label (like 2026-06) and the revenue total for that period.
  • DoctorRevenueDto summarizes one doctor's contribution: their email, total revenue, and how many bills they created.
  • AnalyticsResponseDto wraps everything together. The DoctorBreakdown property is nullable because only Admins receive it.
  • Save all three files.
  • Confirm they compile by running this from the HospitalApi directory:
dotnet build

You should see a "Build succeeded" message with zero errors.

Seeing a build error?

  • Check that all three files are inside the HospitalApi/DTOs/ folder. The namespace HospitalApi.DTOs must match the folder structure.
  • Verify that AnalyticsResponseDto.cs references RevenueDataPointDto and DoctorRevenueDto by their exact class names.

Ask for help debugging the build error.

Build the AnalyticsController

The controller contains the logic that queries bills, groups them by time period, and enforces which data each role can see. The class-level [Authorize(Roles = "Admin,Doctor")] attribute blocks Receptionists and BillingStaff entirely.

  • Create a new file called AnalyticsController.cs in HospitalApi/Controllers/ and paste the following code:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.DTOs;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "Admin,Doctor")]
public class AnalyticsController : ControllerBase
{
    private readonly HospitalDbContext _context;

    public AnalyticsController(HospitalDbContext context)
    {
        _context = context;
    }

    [HttpGet("revenue")]
    public async Task<IActionResult> GetRevenue([FromQuery] string period = "monthly")
    {
        // Extract the caller's role and ID from the JWT token
        var callerRole = User.FindFirstValue(ClaimTypes.Role) ?? "";
        var callerId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";

        // Start with all bills that have a creator, include the CreatedBy navigation
        var query = _context.Bills
            .Include(b => b.CreatedBy)
            .Where(b => b.CreatedById != null);

        // Doctors only see their own bills
        if (callerRole == "Doctor")
        {
            query = query.Where(b => b.CreatedById == callerId);
        }

        var bills = await query.ToListAsync();

        // Group bills by the requested time period using a switch expression
        var grouped = period switch
        {
            "daily" => bills
                .Where(b => b.CreatedAt >= DateTime.UtcNow.AddDays(-30))
                .GroupBy(b => b.CreatedAt.ToString("yyyy-MM-dd"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
            "yearly" => bills
                .GroupBy(b => b.CreatedAt.ToString("yyyy"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
            _ => bills
                .Where(b => b.CreatedAt >= DateTime.UtcNow.AddMonths(-12))
                .GroupBy(b => b.CreatedAt.ToString("yyyy-MM"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
        };

        // Build the response with totals and time-series data
        var response = new AnalyticsResponseDto
        {
            TotalRevenue = bills.Sum(b => b.TotalAmount),
            BillCount = bills.Count,
            RevenueOverTime = grouped,
        };

        // Admin gets a breakdown of revenue by doctor
        if (callerRole == "Admin")
        {
            response.DoctorBreakdown = bills
                .GroupBy(b => b.CreatedBy?.Email ?? "Unknown")
                .Select(g => new DoctorRevenueDto
                {
                    DoctorEmail = g.Key,
                    TotalRevenue = g.Sum(b => b.TotalAmount),
                    BillCount = g.Count(),
                })
                .OrderByDescending(d => d.TotalRevenue)
                .ToList();
        }

        return Ok(response);
    }
}

What does this controller do?

  • The [Authorize(Roles = "Admin,Doctor")] attribute on the class means any request without a valid Admin or Doctor JWT is rejected with a 403.
  • ClaimTypes.Role and ClaimTypes.NameIdentifier extract the caller's role and user ID from the JWT token. This determines what data they can see.
  • The period switch expression groups bills by day (last 30 days), month (last 12 months), or year (all time) using LINQ GroupBy.
  • The DoctorBreakdown section only runs for Admins. It groups bills by the creator's email and ranks doctors by total revenue.

Why load bills into memory instead of grouping in the database?

LINQ GroupBy with date formatting (like ToString("yyyy-MM")) doesn't translate cleanly to SQL across all database providers. Loading bills into memory and grouping client-side avoids PostgreSQL-specific date functions.

For a hospital system with thousands of bills, this performs well. If you were dealing with millions of rows, you'd move to raw SQL with database-specific date truncation.

  • Save AnalyticsController.cs.
  • Confirm the project still compiles by running:
dotnet build

You should see "Build succeeded" with zero errors and zero warnings.

Build fails after adding the controller?

  • If you see an error about HospitalDbContext, check that your HospitalDbContext class has a DbSet<Bill> property named Bills.
  • If you see errors about TotalAmount or CreatedAt, verify these property names match your Bill model exactly. Your model may use slightly different names.
  • Check the using HospitalApi.DTOs; import is present at the top of the controller.

Help me fix my AnalyticsController build error.

Test the analytics endpoint

The controller compiles, but you need to confirm it returns real data. You'll call the endpoint from the browser console using the token from your logged-in session.

  • Restart the API by stopping the current server with Ctrl+C and running:
dotnet run
  • Switch to your browser and log in as an Admin user through the frontend.
  • Open the browser developer console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
  • Test the analytics endpoint by pasting this code into the console:
const token = JSON.parse(localStorage.getItem("user")).token;
fetch("/api/analytics/revenue?period=monthly", { headers: { "Authorization": `Bearer ${token}` } })
  .then(r => r.json()).then(console.log);

What does this test code do?

It grabs the JWT token from your browser's local storage (where the frontend saved it at login), then sends an authenticated GET request to the analytics endpoint. The response is logged to the console as a JSON object.

You should see a JSON object in the console with totalRevenue, billCount, revenueOverTime (an array), and doctorBreakdown (an array, since you're logged in as Admin).

No revenue data yet?

If totalRevenue is 0 and the arrays are empty, that's expected. Bills created before you added the CreatedById field have null values and are filtered out. Create a new bill through the frontend, then run the test again to see data appear.

Getting a 401 or 403 response?

  • A 401 means the token is missing or expired. Log out and log back in through the frontend to refresh it.
  • A 403 means the token's role claim isn't Admin or Doctor. Make sure you're logged in with an Admin account.
  • Check that your JWT configuration in Program.cs includes the role claim in the token.

Help me debug my 401/403 analytics endpoint issue.

✔️ Awesome, I've got everything!

Great. Double check you've saved all your files before moving on.

ⓧ I'd like to double check the full code

Here's the complete code for all the files created in this step.

namespace HospitalApi.DTOs;

public class RevenueDataPointDto
{
    public string Period { get; set; } = string.Empty;
    public decimal Revenue { get; set; }
}
namespace HospitalApi.DTOs;

public class DoctorRevenueDto
{
    public string DoctorEmail { get; set; } = string.Empty;
    public decimal TotalRevenue { get; set; }
    public int BillCount { get; set; }
}
namespace HospitalApi.DTOs;

public class AnalyticsResponseDto
{
    public decimal TotalRevenue { get; set; }
    public int BillCount { get; set; }
    public List<RevenueDataPointDto> RevenueOverTime { get; set; } = new();
    public List<DoctorRevenueDto>? DoctorBreakdown { get; set; }
}
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.DTOs;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize(Roles = "Admin,Doctor")]
public class AnalyticsController : ControllerBase
{
    private readonly HospitalDbContext _context;

    public AnalyticsController(HospitalDbContext context)
    {
        _context = context;
    }

    [HttpGet("revenue")]
    public async Task<IActionResult> GetRevenue([FromQuery] string period = "monthly")
    {
        var callerRole = User.FindFirstValue(ClaimTypes.Role) ?? "";
        var callerId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";

        var query = _context.Bills
            .Include(b => b.CreatedBy)
            .Where(b => b.CreatedById != null);

        if (callerRole == "Doctor")
        {
            query = query.Where(b => b.CreatedById == callerId);
        }

        var bills = await query.ToListAsync();

        var grouped = period switch
        {
            "daily" => bills
                .Where(b => b.CreatedAt >= DateTime.UtcNow.AddDays(-30))
                .GroupBy(b => b.CreatedAt.ToString("yyyy-MM-dd"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
            "yearly" => bills
                .GroupBy(b => b.CreatedAt.ToString("yyyy"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
            _ => bills
                .Where(b => b.CreatedAt >= DateTime.UtcNow.AddMonths(-12))
                .GroupBy(b => b.CreatedAt.ToString("yyyy-MM"))
                .Select(g => new RevenueDataPointDto { Period = g.Key, Revenue = g.Sum(b => b.TotalAmount) })
                .OrderBy(r => r.Period)
                .ToList(),
        };

        var response = new AnalyticsResponseDto
        {
            TotalRevenue = bills.Sum(b => b.TotalAmount),
            BillCount = bills.Count,
            RevenueOverTime = grouped,
        };

        if (callerRole == "Admin")
        {
            response.DoctorBreakdown = bills
                .GroupBy(b => b.CreatedBy?.Email ?? "Unknown")
                .Select(g => new DoctorRevenueDto
                {
                    DoctorEmail = g.Key,
                    TotalRevenue = g.Sum(b => b.TotalAmount),
                    BillCount = g.Count(),
                })
                .OrderByDescending(d => d.TotalRevenue)
                .ToList();
        }

        return Ok(response);
    }
}

Your analytics API is live and returning role-filtered revenue data. Next up, you'll build a React dashboard that turns this JSON into an interactive bar chart with period switching.

Build the Analytics Dashboard UI

Your analytics API is live and returning revenue data grouped by period. The backend knows how to enforce role-based access. Now it is time to give that data a visual home.

In this step, you will build the frontend AnalyticsPage component using Recharts to render a bar chart, summary cards, and a conditional doctor breakdown table. You will also wire the page into the existing routing and navigation so the right roles can access it.

In this step, get ready to:
  • Create the AnalyticsPage component with a period selector, Recharts bar chart, and summary table.
  • Add the Analytics route to App.tsx using the existing RoleRoute guard.
  • Add an Analytics link to the Navbar for Admin and Doctor roles.
Create AnalyticsPage.tsx

This component fetches revenue data from the API you built in Step 2, lets the user toggle between Daily, Monthly, and Yearly views, and renders the results as a bar chart. For Admin users, it also displays a doctor breakdown table below the chart.

  • Create a new file called AnalyticsPage.tsx inside hospital-frontend/src/pages/.
  • Paste the following code into the file:
import { useEffect, useState } from "react";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";

interface RevenueDataPoint {
  Period: string;
  Revenue: number;
}

interface DoctorRevenue {
  doctorEmail: string;
  totalRevenue: number;
  billCount: number;
}

interface AnalyticsResponse {
  totalRevenue: number;
  billCount: number;
  revenueOverTime: RevenueDataPoint[];
  doctorBreakdown: DoctorRevenue[] | null;
}

export default function AnalyticsPage() {
  const { user } = useAuth();
  const [period, setPeriod] = useState("monthly");
  const [data, setData] = useState<AnalyticsResponse | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  const fetchAnalytics = async (selectedPeriod: string) => {
    setLoading(true);
    try {
      const res = await axiosClient.get(
        `/api/analytics/revenue?period=${selectedPeriod}`
      );
      setData(res.data);
      setError("");
    } catch {
      setError("Failed to load analytics.");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchAnalytics(period);
  }, [period]);

  if (loading) return <div style={{ padding: 20 }}>Loading analytics...</div>;

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Analytics Dashboard</h1>
        <div style={{ display: "flex", gap: 8 }}>
          {["daily", "monthly", "yearly"].map((p) => (
            <button
              key={p}
              onClick={() => setPeriod(p)}
              style={{
                padding: "8px 16px",
                backgroundColor: period === p ? "#1a1a2e" : "#eee",
                color: period === p ? "#fff" : "#333",
                border: "none",
                borderRadius: 4,
                cursor: "pointer",
                textTransform: "capitalize",
              }}
            >
              {p}
            </button>
          ))}
        </div>
      </div>

      {error && <p style={{ color: "red", marginBottom: 12 }}>{error}</p>}

      {data && (
        <>
          {/* Summary cards */}
          <div style={{ display: "flex", gap: 20, marginBottom: 24 }}>
            <div
              style={{
                padding: 16,
                border: "1px solid #ccc",
                borderRadius: 4,
                flex: 1,
              }}
            >
              <div style={{ fontSize: 14, color: "#666" }}>Total Revenue</div>
              <div style={{ fontSize: 24, fontWeight: "bold" }}>
                ${data.totalRevenue.toFixed(2)}
              </div>
            </div>
            <div
              style={{
                padding: 16,
                border: "1px solid #ccc",
                borderRadius: 4,
                flex: 1,
              }}
            >
              <div style={{ fontSize: 14, color: "#666" }}>Total Bills</div>
              <div style={{ fontSize: 24, fontWeight: "bold" }}>
                {data.billCount}
              </div>
            </div>
          </div>

          {/* Revenue bar chart */}
          <div
            style={{
              border: "1px solid #ccc",
              borderRadius: 4,
              padding: 16,
              marginBottom: 24,
            }}
          >
            <h3 style={{ marginTop: 0 }}>Revenue Over Time ({period})</h3>
            {data.revenueOverTime.length === 0 ? (
              <p style={{ color: "#888" }}>
                No revenue data for this period.
              </p>
            ) : (
              <ResponsiveContainer width="100%" height={300}>
                <BarChart
                  data={data.revenueOverTime}
                  margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
                >
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="Period" />
                  <YAxis tickFormatter={(value) => `$${value}`} />
                  <Tooltip
                    formatter={(value: number) => [`$${value.toFixed(2)}`, "Revenue"]}
                  />
                  <Bar dataKey="Revenue" fill="#3498db" />
                </BarChart>
              </ResponsiveContainer>
            )}
          </div>

          {/* Doctor breakdown table (Admin only) */}
          {user?.role === "Admin" && data.doctorBreakdown && (
            <div
              style={{
                border: "1px solid #ccc",
                borderRadius: 4,
                padding: 16,
              }}
            >
              <h3 style={{ marginTop: 0 }}>Revenue by Doctor</h3>
              <table style={{ width: "100%", borderCollapse: "collapse" }}>
                <thead>
                  <tr>
                    <th style={thStyle}>Doctor</th>
                    <th style={thStyle}>Revenue</th>
                    <th style={thStyle}>Bills</th>
                  </tr>
                </thead>
                <tbody>
                  {data.doctorBreakdown.map((d) => (
                    <tr key={d.doctorEmail}>
                      <td style={tdStyle}>{d.doctorEmail}</td>
                      <td style={tdStyle}>${d.totalRevenue.toFixed(2)}</td>
                      <td style={tdStyle}>{d.billCount}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </>
      )}
    </div>
  );
}

const thStyle = {
  borderBottom: "2px solid #ccc",
  textAlign: "left" as const,
  padding: 8,
};

const tdStyle = {
  borderBottom: "1px solid #eee",
  padding: 8,
};

What does this code do?

  • The three TypeScript interfaces (RevenueDataPoint, DoctorRevenue, AnalyticsResponse) define the shape of the data the API returns, giving you type safety throughout the component.
  • fetchAnalytics calls the /api/analytics/revenue endpoint with the selected period and stores the result in state.
  • The useEffect re-fetches whenever the learner clicks a different period button (Daily, Monthly, Yearly).
  • ResponsiveContainer wraps the BarChart so it scales to fill the available width while keeping a fixed 300px height.
  • The doctor breakdown table only renders when user?.role === "Admin" and the API has returned doctorBreakdown data.
  • Save AnalyticsPage.tsx.
  • You will not see the page yet because it is not connected to a route. The next substep wires it into the app.

Seeing red squiggles in the file?

  • If TypeScript cannot find recharts, make sure you ran npm install recharts react-is in the hospital-frontend directory in Step 1.
  • If axiosClient or useAuth cannot be resolved, double-check that your import paths match your project folder structure.

Why can't my component find the recharts imports?

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

import { useEffect, useState } from "react";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
} from "recharts";

interface RevenueDataPoint {
  Period: string;
  Revenue: number;
}

interface DoctorRevenue {
  doctorEmail: string;
  totalRevenue: number;
  billCount: number;
}

interface AnalyticsResponse {
  totalRevenue: number;
  billCount: number;
  revenueOverTime: RevenueDataPoint[];
  doctorBreakdown: DoctorRevenue[] | null;
}

export default function AnalyticsPage() {
  const { user } = useAuth();
  const [period, setPeriod] = useState("monthly");
  const [data, setData] = useState<AnalyticsResponse | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState("");

  const fetchAnalytics = async (selectedPeriod: string) => {
    setLoading(true);
    try {
      const res = await axiosClient.get(
        `/api/analytics/revenue?period=${selectedPeriod}`
      );
      setData(res.data);
      setError("");
    } catch {
      setError("Failed to load analytics.");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchAnalytics(period);
  }, [period]);

  if (loading) return <div style={{ padding: 20 }}>Loading analytics...</div>;

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Analytics Dashboard</h1>
        <div style={{ display: "flex", gap: 8 }}>
          {["daily", "monthly", "yearly"].map((p) => (
            <button
              key={p}
              onClick={() => setPeriod(p)}
              style={{
                padding: "8px 16px",
                backgroundColor: period === p ? "#1a1a2e" : "#eee",
                color: period === p ? "#fff" : "#333",
                border: "none",
                borderRadius: 4,
                cursor: "pointer",
                textTransform: "capitalize",
              }}
            >
              {p}
            </button>
          ))}
        </div>
      </div>

      {error && <p style={{ color: "red", marginBottom: 12 }}>{error}</p>}

      {data && (
        <>
          {/* Summary cards */}
          <div style={{ display: "flex", gap: 20, marginBottom: 24 }}>
            <div
              style={{
                padding: 16,
                border: "1px solid #ccc",
                borderRadius: 4,
                flex: 1,
              }}
            >
              <div style={{ fontSize: 14, color: "#666" }}>Total Revenue</div>
              <div style={{ fontSize: 24, fontWeight: "bold" }}>
                ${data.totalRevenue.toFixed(2)}
              </div>
            </div>
            <div
              style={{
                padding: 16,
                border: "1px solid #ccc",
                borderRadius: 4,
                flex: 1,
              }}
            >
              <div style={{ fontSize: 14, color: "#666" }}>Total Bills</div>
              <div style={{ fontSize: 24, fontWeight: "bold" }}>
                {data.billCount}
              </div>
            </div>
          </div>

          {/* Revenue bar chart */}
          <div
            style={{
              border: "1px solid #ccc",
              borderRadius: 4,
              padding: 16,
              marginBottom: 24,
            }}
          >
            <h3 style={{ marginTop: 0 }}>Revenue Over Time ({period})</h3>
            {data.revenueOverTime.length === 0 ? (
              <p style={{ color: "#888" }}>
                No revenue data for this period.
              </p>
            ) : (
              <ResponsiveContainer width="100%" height={300}>
                <BarChart
                  data={data.revenueOverTime}
                  margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
                >
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="Period" />
                  <YAxis tickFormatter={(value) => `$${value}`} />
                  <Tooltip
                    formatter={(value: number) => [`$${value.toFixed(2)}`, "Revenue"]}
                  />
                  <Bar dataKey="Revenue" fill="#3498db" />
                </BarChart>
              </ResponsiveContainer>
            )}
          </div>

          {/* Doctor breakdown table (Admin only) */}
          {user?.role === "Admin" && data.doctorBreakdown && (
            <div
              style={{
                border: "1px solid #ccc",
                borderRadius: 4,
                padding: 16,
              }}
            >
              <h3 style={{ marginTop: 0 }}>Revenue by Doctor</h3>
              <table style={{ width: "100%", borderCollapse: "collapse" }}>
                <thead>
                  <tr>
                    <th style={thStyle}>Doctor</th>
                    <th style={thStyle}>Revenue</th>
                    <th style={thStyle}>Bills</th>
                  </tr>
                </thead>
                <tbody>
                  {data.doctorBreakdown.map((d) => (
                    <tr key={d.doctorEmail}>
                      <td style={tdStyle}>{d.doctorEmail}</td>
                      <td style={tdStyle}>${d.totalRevenue.toFixed(2)}</td>
                      <td style={tdStyle}>{d.billCount}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
        </>
      )}
    </div>
  );
}

const thStyle = {
  borderBottom: "2px solid #ccc",
  textAlign: "left" as const,
  padding: 8,
};

const tdStyle = {
  borderBottom: "1px solid #eee",
  padding: 8,
};
Add the route and Navbar link

The page component exists but is not reachable yet. You need to register it as a route and add a navigation link so users can find it.

  • Open App.tsx in the hospital-frontend/src/ folder.
  • Add the following import near the top of the file, alongside your other page imports:
import AnalyticsPage from "./pages/AnalyticsPage";
  • Inside the <Routes> block, add the following route. Place it alongside your other RoleRoute-wrapped routes:
<Route
  path="/analytics"
  element={
    <RoleRoute roles={["Admin", "Doctor"]}>
      <AnalyticsPage />
    </RoleRoute>
  }
/>

What does this route do?

The RoleRoute guard checks the logged-in user's role before rendering the child component. Only users with the Admin or Doctor role can reach /analytics. Anyone else gets redirected.

  • Save App.tsx.
  • Now open Navbar.tsx in hospital-frontend/src/components/.
  • Add the following link next to the existing Staff Management link, inside the section that renders navigation for logged-in users:
{(user.role === "Admin" || user.role === "Doctor") && (
  <Link to="/analytics" style={{ color: "#ccc", textDecoration: "none" }}>
    Analytics
  </Link>
)}

What does this conditional render?

The condition checks user.role before rendering the link. If the logged-in user is a Receptionist or BillingStaff, they never see the Analytics option in the navigation bar.

  • Save Navbar.tsx.

Getting a compile error after adding the route?

  • Make sure the import AnalyticsPage from "./pages/AnalyticsPage" line points to the exact file path where you saved AnalyticsPage.tsx.
  • Confirm that the <Route> element is placed inside the existing <Routes> block, not outside it.

Why am I getting a compile error after adding my analytics route?

Test the full dashboard

Everything is wired up. Time to verify that the Analytics page renders correctly for each role and that the RBAC enforcement works at the UI level.

  • Restart both the API and the frontend development server if they are not already running.
  • Log in as an Admin user.
  • Click Analytics in the Navbar.

You should see the Analytics Dashboard heading, three period buttons (daily, monthly, yearly), summary cards for Total Revenue and Total Bills, and a bar chart. Below the chart, a "Revenue by Doctor" table should appear.

  • Click each period button to confirm the chart updates with different data groupings.
  • Log out and log in as a Doctor.
  • Click Analytics in the Navbar.

The page should load with only the Doctor's own revenue data. The "Revenue by Doctor" table should not appear.

  • Log out and log in as a Receptionist or BillingStaff user.

The Analytics link should not appear in the Navbar. If you navigate to /analytics manually, you should be redirected to /patients.

No data in the chart?

If your bar chart shows "No revenue data for this period", that means no bills have a CreatedById value yet. Create a new bill through the frontend while logged in as a Doctor or Admin. The analytics endpoint filters out bills where CreatedById is null.

Chart not rendering at all?

  • Open the browser console and check for JavaScript errors. A common issue is a mismatched import path for axiosClient or useAuth.
  • Verify that the API is running. If the API server is stopped, the fetch call will fail and you will see the "Failed to load analytics" error message on the page.
  • Check that the recharts package is listed in your package.json dependencies. If not, run npm install recharts react-is again from the hospital-frontend directory.

My Recharts bar chart is not rendering on the Analytics page. Help me debug.

Secret mission

Add a Top Doctors Leaderboard

Your Analytics Dashboard shows revenue over time, but hospital administrators want more. Add a leaderboard that ranks the top 5 doctors by total revenue, displayed as a horizontal bar chart visible only to Admins.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing cloud costs.

Resources you used:

  • Running API and frontend development servers.
  • PostgreSQL database with analytics data.
  • Recharts npm package added to the frontend.

Keep everything running

No action needed. Choose this if you're still actively building or testing the analytics dashboard.

  • Your API and frontend servers continue running in their terminal windows.
  • Your PostgreSQL database persists all billing and analytics data.
  • The Recharts package remains in your hospital-frontend/node_modules directory.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and database persist on disk.

  • Switch to the terminal running the API and press Ctrl+C to stop it.
  • Switch to the terminal running the frontend and press Ctrl+C to stop it.
  • Your project files and PostgreSQL database remain on disk. Run dotnet run and npm start again when you're ready to resume.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your database and project files.

  • Stop both servers by pressing Ctrl+C in each terminal window.
  • Drop the PostgreSQL database by running:
dropdb HospitalDb
  • Delete the project folder. On macOS/Linux:
cd ~/Desktop
rm -rf HospitalApi hospital-frontend

On Windows:

cd ~\Desktop
rmdir /s /q HospitalApi
rmdir /s /q hospital-frontend
  • This removes all source code, migrations, node modules, and configuration files.

Nice Work!

Nice Work!

You just built a role-enforced analytics dashboard for the Korutla Hospital system. This is the same pattern used in every SaaS product with admin reporting.

You learned how to:

  • Added a CreatedById field to the Bill model linking every bill to the user who created it, with a database migration.
  • Built an AnalyticsController with LINQ GroupBy aggregation that returns revenue data grouped by day, month, or year, with role enforcement ensuring Admins see all data and Doctors see only their own.
  • Created a React Analytics Dashboard page with a Recharts bar chart, period selector, and conditional doctor breakdown table, fully integrated with the existing RBAC routing and navigation.
  • Secret Mission: Added a top-performing doctors leaderboard with a horizontal bar chart, visible only to Admins.

Ready to quiz yourself?