Hospital Billing and Admin Dashboard

Add billing, invoicing, and admin management to a hospital system.

Introduction

30 Second Summary

Every hospital visit ends with a bill. Behind the scenes, staff need a reliable system to create itemized charges, manage user accounts, and generate professional invoices that patients can take home.

In this project, you will extend an existing hospital management system with a billing module and an admin dashboard. You will build new database tables, role-based API endpoints, and polished frontend pages for creating bills and printing invoices directly from the browser.

What You'll Build

A billing and admin extension for the Korutla Hospital Management System, featuring new Entity Framework Core database entities, two new ASP.NET Core API controllers, and three new React pages including a printable invoice.

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

  • Bill and BillItem database tables created through an Entity Framework Core migration, storing itemized charges alongside existing patient data in PostgreSQL.
  • Billing and Admin API endpoints with role-based authorization for creating itemized bills, listing patients' billing history, and managing staff accounts through the web UI.
  • Admin Dashboard, Patient Details, and Invoice pages in React for managing staff, creating bills with dynamic item rows, and printing professional invoices using the browser's native print dialog.
  • Secret Mission: Add payment recording so staff can track partial and full payments against bills.

Do I need to complete another project first?

Yes. This project builds directly on the "Hospital System with ASP.NET Core and React" project. You should have that system running locally with all tools installed, including VS Code, the .NET 10 SDK, PostgreSQL, and Node.js.

Add Billing Models and Run the Database Migration

The hospital system can register and manage patients, but it has no way to track what services a patient receives or how much they owe. Every hospital needs a billing system that ties charges to individual patients.

In this step, you will create new C# entity models for bills and bill items, register them with Entity Framework Core, and run a database migration to create the new tables in PostgreSQL.

In this step, get ready to:
  • Create entity models for bills, bill items, and their supporting enums.
  • Update the database context with new DbSets and precision configuration for money columns.
  • Run an EF Core migration to create the new tables in the database.
Create the billing entity models

Billing needs two enums to classify data: one for the type of charge (consultation, lab work, medicine) and one for payment status (unpaid, partially paid, paid). These enums will be used by the entity models you create next.

  • In the VS Code Explorer sidebar, right-click the Models folder inside HospitalApi.
  • Select New File and name it BillItemCategory.cs.
  • Paste the following code into BillItemCategory.cs:
namespace HospitalApi.Models;

public enum BillItemCategory
{
    Consultation,
    Lab,
    Medicine,
    Room,
    Other
}
  • Create another file in the same Models folder named PaymentStatus.cs.
  • Paste the following code into PaymentStatus.cs:
namespace HospitalApi.Models;

public enum PaymentStatus
{
    Unpaid,
    PartiallyPaid,
    Paid
}

What do these enums do?

BillItemCategory classifies each line item on a bill. A consultation fee, a lab test, and a medicine charge are all different categories.

PaymentStatus tracks whether a bill has been paid. A bill starts as Unpaid and can move to PartiallyPaid or Paid as payments come in.

You won't see a change yet. The next two files use these enums to define the actual database models.

  • Create a new file in the Models folder named BillItem.cs.
  • Paste the following code into BillItem.cs:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

public class BillItem
{
    public int Id { get; set; }

    public int BillId { get; set; }

    public BillItemCategory Category { get; set; }

    [Required]
    [MaxLength(200)]
    public string Description { get; set; } = string.Empty;

    public int Quantity { get; set; } = 1;

    public decimal UnitPrice { get; set; }

    public decimal TotalPrice { get; set; }

    public Bill Bill { get; set; } = null!;
}

What does this model represent?

A BillItem is a single line item on a hospital bill. It captures what the charge is for (Category), a text Description, how many units were used (Quantity), and the per-unit price.

The TotalPrice field stores the result of Quantity * UnitPrice. The Bill navigation property links each item back to its parent bill.

You won't see a change yet. The next file creates the parent Bill model that ties everything together.

  • Create one more file in the Models folder named Bill.cs.
  • Paste the following code into Bill.cs:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

public class Bill
{
    public int Id { get; set; }

    [Required]
    [MaxLength(20)]
    public string BillNumber { get; set; } = string.Empty;

    public int PatientId { get; set; }

    [Required]
    public string CreatedById { get; set; } = string.Empty;

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    public decimal SubTotal { get; set; }

    public decimal TaxRate { get; set; }

    public decimal TaxAmount { get; set; }

    public decimal TotalAmount { get; set; }

    public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;

    public decimal AmountPaid { get; set; }

    [MaxLength(500)]
    public string Notes { get; set; } = string.Empty;

    public Patient Patient { get; set; } = null!;

    public List<BillItem> Items { get; set; } = new();
}

What does this model represent?

A Bill is the top-level container for a patient's charges. BillNumber gives each bill a unique identifier like KRL-B-260001. PatientId links the bill to a specific patient.

The money fields (SubTotal, TaxAmount, TotalAmount, AmountPaid) use the decimal type for exact currency arithmetic. The Items list holds all the individual line items on this bill.

  • Save all four files: BillItemCategory.cs, PaymentStatus.cs, BillItem.cs, and Bill.cs.
  • Check the VS Code Problems panel at the bottom of the window. You should see no red errors related to these files.

Seeing red squiggles in your model files?

  • Make sure all four files are saved. Unsaved files show a dot on the tab name.
  • Verify that each file is inside the HospitalApi/Models/ folder and uses the namespace HospitalApi.Models.
  • Check that BillItem.cs and Bill.cs both start with using System.ComponentModel.DataAnnotations; on the first line.

Why are my model files showing errors?

✔️ Awesome, I've got everything!

Great. Make sure all four model files are saved before moving on.

ⓧ I'd like to double check the full code

namespace HospitalApi.Models;

public enum BillItemCategory
{
    Consultation,
    Lab,
    Medicine,
    Room,
    Other
}
namespace HospitalApi.Models;

public enum PaymentStatus
{
    Unpaid,
    PartiallyPaid,
    Paid
}
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

public class BillItem
{
    public int Id { get; set; }

    public int BillId { get; set; }

    public BillItemCategory Category { get; set; }

    [Required]
    [MaxLength(200)]
    public string Description { get; set; } = string.Empty;

    public int Quantity { get; set; } = 1;

    public decimal UnitPrice { get; set; }

    public decimal TotalPrice { get; set; }

    public Bill Bill { get; set; } = null!;
}
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.Models;

public class Bill
{
    public int Id { get; set; }

    [Required]
    [MaxLength(20)]
    public string BillNumber { get; set; } = string.Empty;

    public int PatientId { get; set; }

    [Required]
    public string CreatedById { get; set; } = string.Empty;

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    public decimal SubTotal { get; set; }

    public decimal TaxRate { get; set; }

    public decimal TaxAmount { get; set; }

    public decimal TotalAmount { get; set; }

    public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Unpaid;

    public decimal AmountPaid { get; set; }

    [MaxLength(500)]
    public string Notes { get; set; } = string.Empty;

    public Patient Patient { get; set; } = null!;

    public List<BillItem> Items { get; set; } = new();
}
Register the new models in HospitalDbContext

Entity Framework Core needs to know about your new models before it can create database tables for them. You register models by adding DbSet properties and configuring column-level details like decimal precision in OnModelCreating.

  • Open HospitalApi/Data/HospitalDbContext.cs.
  • Find this existing line:
    public DbSet<Patient> Patients => Set<Patient>();
  • Add these two lines directly below it:
    public DbSet<Bill> Bills => Set<Bill>();
    public DbSet<BillItem> BillItems => Set<BillItem>();

What are DbSets?

Each DbSet property tells EF Core to map a C# class to a database table. DbSet<Bill> Bills creates a Bills table, and DbSet<BillItem> BillItems creates a BillItems table. This follows the same pattern as the existing Patients DbSet.

Next, you need to configure how EF Core handles the decimal columns and relationships for these new models. Money values need explicit precision settings so PostgreSQL stores them accurately.

  • Inside the OnModelCreating method, find the closing }); of the existing builder.Entity<Patient> block.
  • Add the following two configuration blocks directly below it:
        builder.Entity<Bill>(entity =>
        {
            entity.HasIndex(b => b.BillNumber).IsUnique();
            entity.Property(b => b.SubTotal).HasPrecision(18, 2);
            entity.Property(b => b.TaxRate).HasPrecision(5, 2);
            entity.Property(b => b.TaxAmount).HasPrecision(18, 2);
            entity.Property(b => b.TotalAmount).HasPrecision(18, 2);
            entity.Property(b => b.AmountPaid).HasPrecision(18, 2);
            entity.Property(b => b.PaymentStatus).HasConversion<string>();
            entity.HasOne(b => b.Patient)
                  .WithMany()
                  .HasForeignKey(b => b.PatientId);
        });

        builder.Entity<BillItem>(entity =>
        {
            entity.Property(i => i.UnitPrice).HasPrecision(18, 2);
            entity.Property(i => i.TotalPrice).HasPrecision(18, 2);
            entity.Property(i => i.Category).HasConversion<string>();
            entity.HasOne(i => i.Bill)
                  .WithMany(b => b.Items)
                  .HasForeignKey(i => i.BillId);
        });

What does this configuration do?

  • HasIndex(b => b.BillNumber).IsUnique() prevents duplicate bill numbers in the database.
  • HasPrecision(18, 2) tells PostgreSQL to store money columns as numeric(18,2) with exactly 2 decimal places. Without this, EF Core uses a default precision that can cause rounding errors.
  • HasConversion<string>() stores enums as readable text ("Unpaid", "Consultation") instead of integer values (0, 1). This makes the database easier to query directly.
  • HasOne/WithMany sets up the foreign key relationships: each Bill belongs to one Patient, and each BillItem belongs to one Bill.

Why is TaxRate precision (5, 2) instead of (18, 2)?

Tax rates are percentages, not dollar amounts. A precision of (5, 2) allows values up to 999.99%, which is more than enough for any realistic tax rate. Money columns use (18, 2) because bill totals can be much larger numbers.

  • Save HospitalDbContext.cs.
  • Check the Problems panel for any red errors. You should see none.

Seeing errors in HospitalDbContext.cs?

  • Make sure the file still has using HospitalApi.Models; at the top. This import is needed for the Bill and BillItem types.
  • Verify your new configuration blocks are inside the OnModelCreating method and after the existing builder.Entity<Patient> block, not nested inside it.
  • Check that you saved all four model files from the previous substep. The DbContext references those classes.

Help me fix errors in my HospitalDbContext configuration.

✔️ Awesome, I've got everything!

Great. Make sure HospitalDbContext.cs is saved before running the migration.

ⓧ I'd like to double check the full code

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Models;

namespace HospitalApi.Data;

public class HospitalDbContext : IdentityDbContext<IdentityUser>
{
    public HospitalDbContext(DbContextOptions<HospitalDbContext> options) : base(options) { }

    public DbSet<Patient> Patients => Set<Patient>();
    public DbSet<Bill> Bills => Set<Bill>();
    public DbSet<BillItem> BillItems => Set<BillItem>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity<Patient>(entity =>
        {
            entity.HasIndex(p => p.PatientUniqueId).IsUnique();
            entity.Property(p => p.PatientType)
                  .HasConversion<string>();
        });

        builder.Entity<Bill>(entity =>
        {
            entity.HasIndex(b => b.BillNumber).IsUnique();
            entity.Property(b => b.SubTotal).HasPrecision(18, 2);
            entity.Property(b => b.TaxRate).HasPrecision(5, 2);
            entity.Property(b => b.TaxAmount).HasPrecision(18, 2);
            entity.Property(b => b.TotalAmount).HasPrecision(18, 2);
            entity.Property(b => b.AmountPaid).HasPrecision(18, 2);
            entity.Property(b => b.PaymentStatus).HasConversion<string>();
            entity.HasOne(b => b.Patient)
                  .WithMany()
                  .HasForeignKey(b => b.PatientId);
        });

        builder.Entity<BillItem>(entity =>
        {
            entity.Property(i => i.UnitPrice).HasPrecision(18, 2);
            entity.Property(i => i.TotalPrice).HasPrecision(18, 2);
            entity.Property(i => i.Category).HasConversion<string>();
            entity.HasOne(i => i.Bill)
                  .WithMany(b => b.Items)
                  .HasForeignKey(i => i.BillId);
        });
    }
}
Run the database migration

Your C# models and DbContext are ready. Now you need to tell EF Core to generate a migration file and apply it to the PostgreSQL database. This creates the actual Bills and BillItems tables.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick).
  • Make sure you are in the HospitalApi directory by running:
cd ~/Desktop/korutla-hospital/HospitalApi
  • Generate the migration file by running:
dotnet ef migrations add AddBilling

What does this command do?

dotnet ef migrations add compares your current DbContext configuration against the last migration snapshot. It generates a new migration file containing the SQL instructions to create the Bills and BillItems tables. The name AddBilling is a label that describes what this migration does.

You should see a Done. message in the terminal, and a new file appears in the Migrations folder.

Migration command failed?

  • If you see a build error, check that all five files (four models + DbContext) are saved with no syntax errors.
  • Confirm you are running the command from inside the HospitalApi directory, not the parent korutla-hospital folder.
  • Make sure the dotnet-ef tool is still installed from the previous project. Run dotnet ef to check.

Help me fix my EF Core migration error.

  • Apply the migration to the database by running:
dotnet ef database update

You should see output ending with Done. confirming the tables were created.

  • Verify the new tables exist in PostgreSQL by running:
sudo -u postgres psql -d korutla_hospital -c "\dt"

You should see Bills and BillItems listed alongside the existing tables like Patients and the ASP.NET Identity tables.

Don't see the Bills and BillItems tables?

  • Make sure the dotnet ef database update command completed with Done. and no errors.
  • Check that your PostgreSQL service is running. If the database connection fails, the migration cannot apply.
  • Verify you are connecting to the korutla_hospital database in the psql command. The -d korutla_hospital flag targets that specific database.

Help me verify my database migration was applied correctly.

Your database is ready with the new billing tables. Next up, you will build the API endpoints that create, list, and retrieve bills through these tables.

Build the Billing and Admin API Endpoints

Your database now has the Bills and BillItems tables ready to store billing data. The next challenge is giving the frontend a way to create bills and manage staff accounts through the API.

In this step, you'll create Data Transfer Objects (DTOs) that define the shape of API requests and responses. Then you'll build two new controllers: BillingController for creating and retrieving itemized bills, and AdminController for managing user accounts.

In this step, get ready to:
  • Create DTOs for billing requests, billing responses, and user responses.
  • Build a BillingController with endpoints for creating and retrieving bills.
  • Build an AdminController with endpoints for listing and deleting users, and test everything with curl.
Create the billing and admin DTOs

DTOs act as a contract between the API and the frontend. Input DTOs define what data a request must include. Response DTOs control exactly which fields the API sends back.

  • In VS Code, create a new file called CreateBillItemDto.cs inside the HospitalApi/DTOs folder, then paste the following code:
using System.ComponentModel.DataAnnotations;
using HospitalApi.Models;

namespace HospitalApi.DTOs;

// Defines the shape of each line item when creating a bill
public class CreateBillItemDto
{
    [Required]
    public BillItemCategory Category { get; set; }

    [Required]
    [MaxLength(200)]
    public string Description { get; set; } = string.Empty;

    // Defaults to 1 if not specified
    public int Quantity { get; set; } = 1;

    [Required]
    public decimal UnitPrice { get; set; }
}
  • Create another file called CreateBillDto.cs in the same DTOs folder, then paste this code:
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.DTOs;

// Wraps the full bill creation request: tax, notes, and a list of items
public class CreateBillDto
{
    public decimal TaxRate { get; set; }

    [MaxLength(500)]
    public string Notes { get; set; } = string.Empty;

    [Required]
    public List<CreateBillItemDto> Items { get; set; } = new();
}

What do these DTOs do?

CreateBillItemDto defines a single line item on a bill: category (Consultation, Lab, Medicine, etc.), description, quantity, and unit price. The server calculates the total price.

CreateBillDto wraps the full request: a tax rate, optional notes, and a list of CreateBillItemDto items. The [Required] attribute on Items ensures every bill has at least one item.

  • Save both files.
  • Now create the response DTOs. Create a file called BillItemResponseDto.cs in the DTOs folder, then paste this code:
namespace HospitalApi.DTOs;

// Shapes each line item in the API response
public class BillItemResponseDto
{
    public int Id { get; set; }
    public string Category { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal TotalPrice { get; set; }
}
  • Create BillResponseDto.cs in the DTOs folder, then paste this code:
namespace HospitalApi.DTOs;

// The full bill response including patient info, totals, and line items
public class BillResponseDto
{
    public int Id { get; set; }
    public string BillNumber { get; set; } = string.Empty;
    public int PatientId { get; set; }
    public string PatientName { get; set; } = string.Empty;
    public string PatientUniqueId { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public decimal SubTotal { get; set; }
    public decimal TaxRate { get; set; }
    public decimal TaxAmount { get; set; }
    public decimal TotalAmount { get; set; }
    // Enums are converted to strings for clean JSON output
    public string PaymentStatus { get; set; } = string.Empty;
    public decimal AmountPaid { get; set; }
    public string Notes { get; set; } = string.Empty;
    public List<BillItemResponseDto> Items { get; set; } = new();
}
  • Create UserResponseDto.cs in the DTOs folder, then paste this code:
namespace HospitalApi.DTOs;

// Shapes user data for the admin endpoints
public class UserResponseDto
{
    public string Id { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
}

Why are Category and PaymentStatus strings in the response DTOs?

The Bill and BillItem models store enums like BillItemCategory and PaymentStatus as C# enum types. The response DTOs use string instead so the JSON output shows readable values like "Consultation" rather than raw integers like 0.

  • Save all three response DTO files.
  • Confirm you now have five new files in the DTOs folder: CreateBillItemDto.cs, CreateBillDto.cs, BillItemResponseDto.cs, BillResponseDto.cs, and UserResponseDto.cs.

Seeing red squiggly lines in your DTO files?

If CreateBillDto.cs shows an error on CreateBillItemDto, make sure you saved CreateBillItemDto.cs first. The C# Dev Kit needs the referenced class to exist before it can resolve the type.

Check that every file uses the namespace HospitalApi.DTOs. A mismatched namespace will prevent the files from seeing each other.

help me fix red squiggly errors in my DTO files

✔️ Awesome, I've got everything!

Great. Make sure all five DTO files are saved before moving on.

ⓧ I'd like to double check the full code

using System.ComponentModel.DataAnnotations;
using HospitalApi.Models;

namespace HospitalApi.DTOs;

public class CreateBillItemDto
{
    [Required]
    public BillItemCategory Category { get; set; }

    [Required]
    [MaxLength(200)]
    public string Description { get; set; } = string.Empty;

    public int Quantity { get; set; } = 1;

    [Required]
    public decimal UnitPrice { get; set; }
}
using System.ComponentModel.DataAnnotations;

namespace HospitalApi.DTOs;

public class CreateBillDto
{
    public decimal TaxRate { get; set; }

    [MaxLength(500)]
    public string Notes { get; set; } = string.Empty;

    [Required]
    public List<CreateBillItemDto> Items { get; set; } = new();
}
namespace HospitalApi.DTOs;

public class BillItemResponseDto
{
    public int Id { get; set; }
    public string Category { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal TotalPrice { get; set; }
}
namespace HospitalApi.DTOs;

public class BillResponseDto
{
    public int Id { get; set; }
    public string BillNumber { get; set; } = string.Empty;
    public int PatientId { get; set; }
    public string PatientName { get; set; } = string.Empty;
    public string PatientUniqueId { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public decimal SubTotal { get; set; }
    public decimal TaxRate { get; set; }
    public decimal TaxAmount { get; set; }
    public decimal TotalAmount { get; set; }
    public string PaymentStatus { get; set; } = string.Empty;
    public decimal AmountPaid { get; set; }
    public string Notes { get; set; } = string.Empty;
    public List<BillItemResponseDto> Items { get; set; } = new();
}
namespace HospitalApi.DTOs;

public class UserResponseDto
{
    public string Id { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
}
Create the BillingController

The BillingController handles three things: creating a new bill with auto-generated bill numbers, listing all bills for a patient, and fetching a single bill by ID.

  • Create a new file called BillingController.cs inside the HospitalApi/Controllers folder, then paste the following code:
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Data;
using HospitalApi.DTOs;
using HospitalApi.Models;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class BillingController : ControllerBase
{
    private readonly HospitalDbContext _context;

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

    // POST /api/billing/{patientId} - Create a new bill for a patient
    [HttpPost("{patientId}")]
    [Authorize(Roles = "Admin,Doctor,Receptionist,Billing")]
    public async Task<IActionResult> CreateBill(int patientId, [FromBody] CreateBillDto dto)
    {
        var patient = await _context.Patients.FindAsync(patientId);
        if (patient == null)
        {
            return NotFound(new { message = "Patient not found." });
        }

        // Grab the logged-in user's ID from the JWT token
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var billNumber = await GenerateBillNumber();

        // Calculate each item's total price server-side
        var items = dto.Items.Select(i => new BillItem
        {
            Category = i.Category,
            Description = i.Description,
            Quantity = i.Quantity,
            UnitPrice = i.UnitPrice,
            TotalPrice = i.Quantity * i.UnitPrice
        }).ToList();

        // Calculate bill totals from the items
        var subTotal = items.Sum(i => i.TotalPrice);
        var taxAmount = subTotal * (dto.TaxRate / 100);

        var bill = new Bill
        {
            BillNumber = billNumber,
            PatientId = patientId,
            CreatedById = userId,
            CreatedAt = DateTime.UtcNow,
            SubTotal = subTotal,
            TaxRate = dto.TaxRate,
            TaxAmount = taxAmount,
            TotalAmount = subTotal + taxAmount,
            Notes = dto.Notes,
            Items = items
        };

        _context.Bills.Add(bill);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetBill), new { billId = bill.Id }, MapToResponse(bill, patient));
    }

    // GET /api/billing/patient/{patientId} - List all bills for a patient
    [HttpGet("patient/{patientId}")]
    public async Task<IActionResult> GetPatientBills(int patientId)
    {
        var bills = await _context.Bills
            .Include(b => b.Patient)
            .Include(b => b.Items)
            .Where(b => b.PatientId == patientId)
            .OrderByDescending(b => b.CreatedAt)
            .ToListAsync();

        return Ok(bills.Select(b => MapToResponse(b, b.Patient)));
    }

    // GET /api/billing/{billId} - Get a single bill with all its items
    [HttpGet("{billId}")]
    public async Task<IActionResult> GetBill(int billId)
    {
        var bill = await _context.Bills
            .Include(b => b.Patient)
            .Include(b => b.Items)
            .FirstOrDefaultAsync(b => b.Id == billId);

        if (bill == null)
        {
            return NotFound(new { message = "Bill not found." });
        }

        return Ok(MapToResponse(bill, bill.Patient));
    }

    // Generates bill numbers like KRL-B-260001, KRL-B-260002, etc.
    private async Task<string> GenerateBillNumber()
    {
        var year = DateTime.UtcNow.Year % 100;
        var prefix = $"KRL-B-{year:D2}";

        var lastBill = await _context.Bills
            .Where(b => b.BillNumber.StartsWith(prefix))
            .OrderByDescending(b => b.BillNumber)
            .FirstOrDefaultAsync();

        var nextNumber = 1;

        if (lastBill != null)
        {
            var lastNumberStr = lastBill.BillNumber.Substring(prefix.Length);
            if (int.TryParse(lastNumberStr, out var lastNumber))
            {
                nextNumber = lastNumber + 1;
            }
        }

        return $"{prefix}{nextNumber:D4}";
    }

    // Converts a Bill entity into a clean response DTO
    private static BillResponseDto MapToResponse(Bill b, Patient p) => new()
    {
        Id = b.Id,
        BillNumber = b.BillNumber,
        PatientId = b.PatientId,
        PatientName = $"{p.FirstName} {p.LastName}",
        PatientUniqueId = p.PatientUniqueId,
        CreatedAt = b.CreatedAt,
        SubTotal = b.SubTotal,
        TaxRate = b.TaxRate,
        TaxAmount = b.TaxAmount,
        TotalAmount = b.TotalAmount,
        PaymentStatus = b.PaymentStatus.ToString(),
        AmountPaid = b.AmountPaid,
        Notes = b.Notes,
        Items = b.Items.Select(i => new BillItemResponseDto
        {
            Id = i.Id,
            Category = i.Category.ToString(),
            Description = i.Description,
            Quantity = i.Quantity,
            UnitPrice = i.UnitPrice,
            TotalPrice = i.TotalPrice
        }).ToList()
    };
}

What does this controller do?

  • CreateBill handles POST /api/billing/{patientId}. It receives a list of items, calculates each item's total price server-side (quantity times unit price), sums everything into a subtotal, applies the tax rate, and saves the bill. Only Admin, Doctor, Receptionist, and Billing roles can access it.
  • GenerateBillNumber creates sequential bill numbers in the format KRL-B-YYNNNN (for example, KRL-B-260001). This follows the same pattern as the existing patient IDs.
  • GetPatientBills and GetBill fetch bills from the database using .Include() to eagerly load the related Patient and Items in a single query.
  • MapToResponse converts enums to strings using .ToString() so the JSON output is human-readable.

Why does the server calculate totals instead of the frontend?

The frontend sends quantities and unit prices, but the server calculates SubTotal, TaxAmount, and TotalAmount. This prevents billing errors from client-side bugs or manipulation. The server is the single source of truth for money calculations.

  • Save BillingController.cs.

You won't see a change yet. The next chunk adds the AdminController, and then you'll test both controllers together with curl.

Seeing errors on the Authorize attribute or ClaimTypes?

Make sure the file includes using System.Security.Claims; at the top. Without it, ClaimTypes.NameIdentifier won't resolve.

Also check that using HospitalApi.DTOs; and using HospitalApi.Models; are both present. The controller needs both the DTO types and the entity model types.

help me fix build errors in my BillingController

✔️ Awesome, I've got everything!

Great. Double check you've saved the file before continuing.

ⓧ I'd like to double check the full code

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HospitalApi.Data;
using HospitalApi.DTOs;
using HospitalApi.Models;

namespace HospitalApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class BillingController : ControllerBase
{
    private readonly HospitalDbContext _context;

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

    [HttpPost("{patientId}")]
    [Authorize(Roles = "Admin,Doctor,Receptionist,Billing")]
    public async Task<IActionResult> CreateBill(int patientId, [FromBody] CreateBillDto dto)
    {
        var patient = await _context.Patients.FindAsync(patientId);
        if (patient == null) return NotFound(new { message = "Patient not found." });

        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
        var billNumber = await GenerateBillNumber();

        var items = dto.Items.Select(i => new BillItem
        {
            Category = i.Category,
            Description = i.Description,
            Quantity = i.Quantity,
            UnitPrice = i.UnitPrice,
            TotalPrice = i.Quantity * i.UnitPrice
        }).ToList();

        var subTotal = items.Sum(i => i.TotalPrice);
        var taxAmount = subTotal * (dto.TaxRate / 100);

        var bill = new Bill
        {
            BillNumber = billNumber,
            PatientId = patientId,
            CreatedById = userId,
            CreatedAt = DateTime.UtcNow,
            SubTotal = subTotal,
            TaxRate = dto.TaxRate,
            TaxAmount = taxAmount,
            TotalAmount = subTotal + taxAmount,
            Notes = dto.Notes,
            Items = items
        };

        _context.Bills.Add(bill);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetBill), new { billId = bill.Id }, MapToResponse(bill, patient));
    }

    [HttpGet("patient/{patientId}")]
    public async Task<IActionResult> GetPatientBills(int patientId)
    {
        var bills = await _context.Bills
            .Include(b => b.Patient)
            .Include(b => b.Items)
            .Where(b => b.PatientId == patientId)
            .OrderByDescending(b => b.CreatedAt)
            .ToListAsync();
        return Ok(bills.Select(b => MapToResponse(b, b.Patient)));
    }

    [HttpGet("{billId}")]
    public async Task<IActionResult> GetBill(int billId)
    {
        var bill = await _context.Bills
            .Include(b => b.Patient)
            .Include(b => b.Items)
            .FirstOrDefaultAsync(b => b.Id == billId);
        if (bill == null) return NotFound(new { message = "Bill not found." });
        return Ok(MapToResponse(bill, bill.Patient));
    }

    private async Task<string> GenerateBillNumber()
    {
        var year = DateTime.UtcNow.Year % 100;
        var prefix = $"KRL-B-{year:D2}";
        var lastBill = await _context.Bills
            .Where(b => b.BillNumber.StartsWith(prefix))
            .OrderByDescending(b => b.BillNumber)
            .FirstOrDefaultAsync();
        var nextNumber = 1;
        if (lastBill != null)
        {
            var lastNumberStr = lastBill.BillNumber.Substring(prefix.Length);
            if (int.TryParse(lastNumberStr, out var lastNumber)) nextNumber = lastNumber + 1;
        }
        return $"{prefix}{nextNumber:D4}";
    }

    private static BillResponseDto MapToResponse(Bill b, Patient p) => new()
    {
        Id = b.Id,
        BillNumber = b.BillNumber,
        PatientId = b.PatientId,
        PatientName = $"{p.FirstName} {p.LastName}",
        PatientUniqueId = p.PatientUniqueId,
        CreatedAt = b.CreatedAt,
        SubTotal = b.SubTotal,
        TaxRate = b.TaxRate,
        TaxAmount = b.TaxAmount,
        TotalAmount = b.TotalAmount,
        PaymentStatus = b.PaymentStatus.ToString(),
        AmountPaid = b.AmountPaid,
        Notes = b.Notes,
        Items = b.Items.Select(i => new BillItemResponseDto
        {
            Id = i.Id,
            Category = i.Category.ToString(),
            Description = i.Description,
            Quantity = i.Quantity,
            UnitPrice = i.UnitPrice,
            TotalPrice = i.TotalPrice
        }).ToList()
    };
}
Create the AdminController and test the endpoints

The AdminController lets administrators list all users and delete staff accounts through the API. Both endpoints are locked to the Admin role using role-based authorization.

  • Create a new file called AdminController.cs inside the HospitalApi/Controllers folder, then paste the following code:
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")]
public class AdminController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;

    public AdminController(UserManager<IdentityUser> userManager)
    {
        _userManager = userManager;
    }

    // GET /api/admin/users - List all users with their roles
    [HttpGet("users")]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _userManager.Users.ToListAsync();
        var userDtos = new List<UserResponseDto>();

        foreach (var user in users)
        {
            var roles = await _userManager.GetRolesAsync(user);
            userDtos.Add(new UserResponseDto
            {
                Id = user.Id,
                Email = user.Email ?? "",
                Role = roles.FirstOrDefault() ?? ""
            });
        }

        return Ok(userDtos);
    }

    // DELETE /api/admin/users/{id} - Delete a user (blocks default admin deletion)
    [HttpDelete("users/{id}")]
    public async Task<IActionResult> DeleteUser(string id)
    {
        var user = await _userManager.FindByIdAsync(id);
        if (user == null)
        {
            return NotFound(new { message = "User not found." });
        }

        // Prevent accidental deletion of the default admin account
        if (user.Email == "admin@korutla.hospital")
        {
            return BadRequest(new { message = "Cannot delete the default admin account." });
        }

        var result = await _userManager.DeleteAsync(user);
        if (!result.Succeeded)
        {
            return BadRequest(new { errors = result.Errors.Select(e => e.Description) });
        }

        return Ok(new { message = $"User '{user.Email}' deleted." });
    }
}

What does this controller do?

  • [Authorize(Roles = "Admin")] on the class restricts every endpoint in this controller to users with the Admin role. Non-admin users get a 403 Forbidden response.
  • GetUsers fetches all users from ASP.NET Core Identity and looks up each user's role. This powers the staff table on the Admin Dashboard.
  • DeleteUser includes a safety check: it refuses to delete the default admin@korutla.hospital account. This prevents administrators from locking themselves out of the system.
  • Save AdminController.cs.

Why don't you need to register these controllers anywhere?

ASP.NET Core automatically discovers all controllers in the same assembly when AddControllers() is called in Program.cs. You set that up in the previous project, so any new controller you add to the Controllers folder is automatically available.

Seeing errors on UserManager or IdentityUser?

Make sure the file includes using Microsoft.AspNetCore.Identity; at the top. The UserManager<IdentityUser> type comes from that namespace.

help me fix build errors in my AdminController

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before you start testing.

ⓧ I'd like to double check the full code

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")]
public class AdminController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;

    public AdminController(UserManager<IdentityUser> userManager)
    {
        _userManager = userManager;
    }

    [HttpGet("users")]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _userManager.Users.ToListAsync();
        var userDtos = new List<UserResponseDto>();
        foreach (var user in users)
        {
            var roles = await _userManager.GetRolesAsync(user);
            userDtos.Add(new UserResponseDto { Id = user.Id, Email = user.Email ?? "", Role = roles.FirstOrDefault() ?? "" });
        }
        return Ok(userDtos);
    }

    [HttpDelete("users/{id}")]
    public async Task<IActionResult> DeleteUser(string id)
    {
        var user = await _userManager.FindByIdAsync(id);
        if (user == null) return NotFound(new { message = "User not found." });
        if (user.Email == "admin@korutla.hospital") return BadRequest(new { message = "Cannot delete the default admin account." });
        var result = await _userManager.DeleteAsync(user);
        if (!result.Succeeded) return BadRequest(new { errors = result.Errors.Select(e => e.Description) });
        return Ok(new { message = $"User deleted." });
    }
}

Now test both controllers by starting the API and making requests with curl.

  • Open a terminal in VS Code and make sure you are inside the HospitalApi directory.
  • Start the API by running:
dotnet run

Note the URL that appears in the terminal output (for example, http://localhost:5147). You'll use this URL in the curl commands below.

  • Open a second terminal in VS Code by clicking the **+** icon in the terminal panel.
  • Log in as the admin user to get a JWT token by running this command (replace the URL if yours is different):
curl -s -X POST http://localhost:5147/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@korutla.hospital","password":"Admin@123"}'

What should I see?

You should see a JSON response containing a token field with a long string of characters. Copy this token value. You'll paste it into the next curl command.

  • Test the BillingController by creating a bill with two items for an existing patient. Run this command, replacing your-token-here with the token you just copied:
curl -s -X POST http://localhost:5147/api/billing/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token-here" \
  -d '{"taxRate":10,"notes":"Checkup visit","items":[{"category":0,"description":"General consultation","quantity":1,"unitPrice":500},{"category":2,"description":"Paracetamol","quantity":2,"unitPrice":50}]}'

What should I see?

You should see a JSON response with a billNumber (like KRL-B-260001), a subTotal of 600 (500 + 2 x 50), a taxAmount of 60 (10% of 600), and a totalAmount of 660. The paymentStatus should be Unpaid.

Getting a 404 or 401 error?

A 401 means your token is missing or expired. Re-run the login curl command and copy the fresh token.

A 404 on /api/billing/1 means there is no patient with ID 1. Check which patients exist by running curl -s http://localhost:5147/api/patients -H "Authorization: Bearer your-token-here" and use a valid patient ID from the response.

If you get a 404 on the route itself (not the patient), make sure you saved BillingController.cs and restarted the API with dotnet run.

help me debug my curl billing request

Your API is fully operational. You have billing endpoints that create itemized bills with automatic calculations, and admin endpoints for managing staff. Next up, you'll build the frontend pages that call these endpoints.

Build the Admin Dashboard and Patient Details Pages

Your API endpoints are tested and ready to serve data. Right now, the only way to manage staff accounts or create bills is through curl commands in the terminal.

In this step, you will build two new React pages that connect to those endpoints. The Admin Dashboard lets administrators create and delete staff accounts through a web form. The Patient Details page shows a patient's information alongside their billing history and a form for creating new bills.

In this step, get ready to:
  • Create an Admin Dashboard page with a staff creation form and user management table.
  • Create a Patient Details page with billing history and an inline bill creation form.
  • Update the app routes and patient list to connect everything together.
Create the Admin Dashboard page

The Admin Dashboard page gives administrators a web interface to manage staff accounts. It replaces the curl commands you used earlier with a form for creating users and a table for viewing and deleting them.

  • In the VS Code Explorer sidebar, navigate to hospital-frontend/src/pages/.
  • Create a new file called AdminDashboardPage.tsx and paste the following code:
import { useEffect, useState, FormEvent } from "react";
import { Link } from "react-router";
import { useAuth } from "../context/AuthContext";
import axiosClient from "../api/axiosClient";

// Shape of each user returned by the admin API
interface User {
  id: string;
  email: string;
  role: string;
}

export default function AdminDashboardPage() {
  // State for the users list and the creation form fields
  const [users, setUsers] = useState<User[]>([]);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [role, setRole] = useState("Doctor");
  const [message, setMessage] = useState("");
  const [error, setError] = useState("");
  const { user, logout } = useAuth();

  // Fetch all users from the admin endpoint
  const fetchUsers = () => {
    axiosClient.get("/api/admin/users").then((res) => setUsers(res.data));
  };

  // Load users when the component first renders
  useEffect(() => {
    fetchUsers();
  }, []);

  // Submit the create-user form using the existing register endpoint
  const handleCreate = async (e: FormEvent) => {
    e.preventDefault();
    setMessage("");
    setError("");
    try {
      await axiosClient.post("/api/auth/register", { email, password, role });
      setMessage(`User ${email} created with role ${role}.`);
      setEmail("");
      setPassword("");
      fetchUsers();
    } catch {
      setError("Failed to create user. Check inputs and try again.");
    }
  };

  // Delete a user after a confirmation prompt
  const handleDelete = async (id: string, userEmail: string) => {
    if (!confirm(`Delete user ${userEmail}?`)) return;
    try {
      await axiosClient.delete(`/api/admin/users/${id}`);
      fetchUsers();
    } catch {
      setError("Failed to delete user.");
    }
  };

  return (
    <div style={{ padding: 20 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
        <h1>Admin Dashboard</h1>
        <div>
          <span>{user?.email} ({user?.role})</span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>

      <Link to="/patients">Back to Patients</Link>

      {/* Staff creation form */}
      <h2 style={{ marginTop: 20 }}>Create Staff Account</h2>
      {message && <p style={{ color: "green" }}>{message}</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleCreate} style={{ marginBottom: 24, maxWidth: 400 }}>
        <div style={{ marginBottom: 12 }}>
          <label>Email</label><br />
          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Password</label><br />
          <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Role</label><br />
          <select value={role} onChange={(e) => setRole(e.target.value)} style={{ width: "100%", padding: 8 }}>
            <option value="Doctor">Doctor</option>
            <option value="Receptionist">Receptionist</option>
            <option value="Billing">Billing</option>
          </select>
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>Create User</button>
      </form>

      {/* Table listing all users with delete buttons */}
      <h2>All Users</h2>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Email</th>
            <th style={thStyle}>Role</th>
            <th style={thStyle}>Actions</th>
          </tr>
        </thead>
        <tbody>
          {users.map((u) => (
            <tr key={u.id}>
              <td style={tdStyle}>{u.email}</td>
              <td style={tdStyle}>{u.role}</td>
              <td style={tdStyle}>
                {u.email !== "admin@korutla.hospital" && (
                  <button onClick={() => handleDelete(u.id, u.email)} style={{ color: "red" }}>Delete</button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// Reusable table cell styles
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 component manages two responsibilities through one page.

  • fetchUsers calls GET /api/admin/users to load the user list. It runs on first render and again after every create or delete.
  • handleCreate posts to the existing POST /api/auth/register endpoint. No new backend code is needed because the register endpoint already accepts email, password, and role.
  • handleDelete calls DELETE /api/admin/users/{id}. The delete button is hidden for admin@korutla.hospital to match the server-side protection in AdminController.
  • Save AdminDashboardPage.tsx.

You won't see a change in the browser yet. The page needs a route in App.tsx before you can navigate to it.

Seeing red squiggles in VS Code?

  • Make sure the file is saved inside hospital-frontend/src/pages/. A file in the wrong folder cannot resolve the ../context/AuthContext import.
  • Check that the file extension is .tsx, not .ts. Files with JSX must use the .tsx extension.

Help me debug import errors in my AdminDashboardPage.tsx file.

Create the Patient Details page

The Patient Details page is the hub for a single patient. It displays their personal information, lists all their bills, and provides an inline form for creating new bills with dynamic item rows.

  • Create a new file called PatientDetailPage.tsx inside hospital-frontend/src/pages/ and paste the following code:
import { useEffect, useState, FormEvent } from "react";
import { useParams, Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

// TypeScript interfaces matching the API response shapes
interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  address: string;
  patientType: string;
  registeredAt: string;
}

interface BillItem {
  id: number;
  category: string;
  description: string;
  quantity: number;
  unitPrice: number;
  totalPrice: number;
}

interface Bill {
  id: number;
  billNumber: string;
  createdAt: string;
  subTotal: number;
  taxRate: number;
  taxAmount: number;
  totalAmount: number;
  paymentStatus: string;
  amountPaid: number;
  notes: string;
  items: BillItem[];
}

// Shape for new bill items before they are sent to the API
interface NewItem {
  category: number;
  description: string;
  quantity: number;
  unitPrice: number;
}

export default function PatientDetailPage() {
  const { id } = useParams();
  const { user, logout } = useAuth();
  const [patient, setPatient] = useState<Patient | null>(null);
  const [bills, setBills] = useState<Bill[]>([]);
  const [showForm, setShowForm] = useState(false);
  const [taxRate, setTaxRate] = useState(0);
  const [notes, setNotes] = useState("");
  const [items, setItems] = useState<NewItem[]>([
    { category: 0, description: "", quantity: 1, unitPrice: 0 },
  ]);
  const [error, setError] = useState("");

  // Fetch patient details and their bills on mount
  useEffect(() => {
    axiosClient.get(`/api/patients/${id}`).then((res) => setPatient(res.data));
    axiosClient.get(`/api/billing/patient/${id}`).then((res) => setBills(res.data));
  }, [id]);

  // Add a blank item row to the form
  const addItem = () => {
    setItems([...items, { category: 0, description: "", quantity: 1, unitPrice: 0 }]);
  };

  // Remove an item row by index
  const removeItem = (index: number) => {
    setItems(items.filter((_, i) => i !== index));
  };

  // Update a single field on an item row
  const updateItem = (index: number, field: string, value: string | number) => {
    const updated = [...items];
    updated[index] = { ...updated[index], [field]: value };
    setItems(updated);
  };

  // Submit the new bill to the billing API
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      await axiosClient.post(`/api/billing/${id}`, { taxRate, notes, items });
      setShowForm(false);
      setItems([{ category: 0, description: "", quantity: 1, unitPrice: 0 }]);
      setTaxRate(0);
      setNotes("");
      axiosClient.get(`/api/billing/patient/${id}`).then((res) => setBills(res.data));
    } catch {
      setError("Failed to create bill. Check your inputs.");
    }
  };

  if (!patient) return <p style={{ padding: 20 }}>Loading...</p>;

  return (
    <div style={{ padding: 20 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
        <h1>Patient Details</h1>
        <div>
          <span>{user?.email} ({user?.role})</span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>

      {/* Patient info card */}
      <div style={{ marginBottom: 24, padding: 16, border: "1px solid #ccc" }}>
        <p>Patient ID: {patient.patientUniqueId}</p>
        <p>Name: {patient.firstName} {patient.lastName}</p>
        <p>Gender: {patient.gender}</p>
        <p>Date of Birth: {patient.dateOfBirth}</p>
        <p>Phone: {patient.phoneNumber}</p>
        <p>Address: {patient.address}</p>
        <p>Type: {patient.patientType}</p>
        <p>Registered: {new Date(patient.registeredAt).toLocaleDateString()}</p>
      </div>

      {/* Billing history header with toggle button */}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <h2>Billing History</h2>
        <button onClick={() => setShowForm(!showForm)} style={{ padding: "8px 16px" }}>
          {showForm ? "Cancel" : "Create New Bill"}
        </button>
      </div>

      {/* Inline bill creation form, shown when toggled */}
      {showForm && (
        <form onSubmit={handleSubmit} style={{ marginBottom: 24, padding: 16, border: "1px solid #ccc" }}>
          <h3>New Bill</h3>
          {error && <p style={{ color: "red" }}>{error}</p>}
          {items.map((item, index) => (
            <div key={index} style={{ display: "flex", gap: 8, marginBottom: 8, alignItems: "end" }}>
              <div>
                <label>Category</label><br />
                <select value={item.category} onChange={(e) => updateItem(index, "category", parseInt(e.target.value))} style={{ padding: 6 }}>
                  <option value={0}>Consultation</option>
                  <option value={1}>Lab</option>
                  <option value={2}>Medicine</option>
                  <option value={3}>Room</option>
                  <option value={4}>Other</option>
                </select>
              </div>
              <div style={{ flex: 1 }}>
                <label>Description</label><br />
                <input value={item.description} onChange={(e) => updateItem(index, "description", e.target.value)} required style={{ width: "100%", padding: 6 }} />
              </div>
              <div style={{ width: 60 }}>
                <label>Qty</label><br />
                <input type="number" value={item.quantity} onChange={(e) => updateItem(index, "quantity", parseInt(e.target.value) || 1)} min={1} style={{ width: "100%", padding: 6 }} />
              </div>
              <div style={{ width: 100 }}>
                <label>Unit Price</label><br />
                <input type="number" value={item.unitPrice} onChange={(e) => updateItem(index, "unitPrice", parseFloat(e.target.value) || 0)} min={0} step="0.01" required style={{ width: "100%", padding: 6 }} />
              </div>
              <button type="button" onClick={() => removeItem(index)} style={{ color: "red", padding: 6 }}>X</button>
            </div>
          ))}
          <button type="button" onClick={addItem} style={{ marginBottom: 12, padding: "4px 12px" }}>+ Add Item</button>
          <div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
            <div>
              <label>Tax Rate (%)</label><br />
              <input type="number" value={taxRate} onChange={(e) => setTaxRate(parseFloat(e.target.value) || 0)} min={0} step="0.01" style={{ padding: 6 }} />
            </div>
            <div style={{ flex: 1 }}>
              <label>Notes</label><br />
              <input value={notes} onChange={(e) => setNotes(e.target.value)} style={{ width: "100%", padding: 6 }} />
            </div>
          </div>
          <button type="submit" style={{ padding: "8px 24px" }}>Submit Bill</button>
        </form>
      )}

      {/* Billing history table */}
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Bill #</th>
            <th style={thStyle}>Date</th>
            <th style={thStyle}>Total</th>
            <th style={thStyle}>Status</th>
            <th style={thStyle}>Actions</th>
          </tr>
        </thead>
        <tbody>
          {bills.map((b) => (
            <tr key={b.id}>
              <td style={tdStyle}>{b.billNumber}</td>
              <td style={tdStyle}>{new Date(b.createdAt).toLocaleDateString()}</td>
              <td style={tdStyle}>{b.totalAmount.toFixed(2)}</td>
              <td style={tdStyle}>{b.paymentStatus}</td>
              <td style={tdStyle}>
                <Link to={`/bills/${b.id}/invoice`}>View Invoice</Link>
              </td>
            </tr>
          ))}
          {bills.length === 0 && (
            <tr><td style={tdStyle} colSpan={5}>No bills yet.</td></tr>
          )}
        </tbody>
      </table>

      <div style={{ marginTop: 16 }}>
        <Link to="/patients">Back to Patients</Link>
      </div>
    </div>
  );
}

// Reusable table cell styles
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?

This component combines patient information, billing history, and bill creation into a single page.

  • useParams() extracts the patient id from the URL. The component uses this to fetch the patient record and their bills on mount.
  • The items state array holds the dynamic bill item rows. addItem appends a new blank row, removeItem removes one by index, and updateItem changes a single field on a specific row.
  • handleSubmit sends the items, tax rate, and notes to POST /api/billing/{id}. After a successful post, it hides the form, resets state, and re-fetches the billing history.
  • Each bill row in the history table includes a View Invoice link pointing to /bills/{id}/invoice. You will build that invoice page in the next step.

Why does the category dropdown use numbers?

The C# backend accepts the BillItemCategory enum as an integer in the JSON request body. 0 maps to Consultation, 1 to Lab, 2 to Medicine, and so on. The response DTOs convert these back to readable strings.

  • Save PatientDetailPage.tsx.

Like the Admin Dashboard, this page needs a route before it is accessible. You will add routes for both pages next.

Seeing TypeScript errors on the interfaces?

  • Make sure every interface field matches the API response exactly. For example, patientUniqueId is camelCase, not PascalCase. The JSON serializer converts C# properties to camelCase automatically.
  • Check that the file extension is .tsx. TypeScript files containing JSX require this extension.

Help me fix TypeScript errors in my PatientDetailPage component.

Update the routes and patient list

The two new pages exist but are not reachable. You need to register routes for them in App.tsx and add navigation links in PatientsPage.tsx so users can click through to them.

  • Open hospital-frontend/src/App.tsx and replace its entire contents with the following code:
import { BrowserRouter, Routes, Route, Navigate } from "react-router";
import { AuthProvider, useAuth } from "./context/AuthContext";
import LoginPage from "./pages/LoginPage";
import PatientsPage from "./pages/PatientsPage";
import RegisterPatientPage from "./pages/RegisterPatientPage";
import PatientDetailPage from "./pages/PatientDetailPage";
import InvoicePage from "./pages/InvoicePage";
import AdminDashboardPage from "./pages/AdminDashboardPage";
import { ReactNode } from "react";

// Redirects unauthenticated users to the login page
function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  return children;
}

// Restricts access to Admin-role users only
function AdminRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  if (user.role !== "Admin") {
    return <Navigate to="/patients" />;
  }
  return children;
}

function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/patients"
        element={
          <ProtectedRoute>
            <PatientsPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/new"
        element={
          <ProtectedRoute>
            <RegisterPatientPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/:id"
        element={
          <ProtectedRoute>
            <PatientDetailPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/bills/:billId/invoice"
        element={
          <ProtectedRoute>
            <InvoicePage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/admin"
        element={
          <AdminRoute>
            <AdminDashboardPage />
          </AdminRoute>
        }
      />
      <Route path="*" element={<Navigate to="/login" />} />
    </Routes>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <AuthProvider>
        <AppRoutes />
      </AuthProvider>
    </BrowserRouter>
  );
}

What changed in App.tsx?

  • Three new imports bring in PatientDetailPage, InvoicePage, and AdminDashboardPage.
  • AdminRoute is a new wrapper that checks for both authentication and the Admin role. Non-admin users get redirected to /patients.
  • Three new routes are registered: /patients/:id for patient details, /bills/:billId/invoice for invoices, and /admin for the admin dashboard.

Why does InvoicePage show a red squiggle?

The import for InvoicePage will show an error because that file does not exist yet. You will create it in the next step. The error is harmless right now and will disappear once the file is in place.

  • Save App.tsx.

Now you need to update the patients list so users can actually click through to the new Patient Details page.

  • Open hospital-frontend/src/pages/PatientsPage.tsx and replace its entire contents with the following code:
import { useEffect, useState } from "react";
import { Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

// Shape of each patient from the API
interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  patientType: string;
  registeredAt: string;
}

export default function PatientsPage() {
  const [patients, setPatients] = useState<Patient[]>([]);
  const [search, setSearch] = useState("");
  const [typeFilter, setTypeFilter] = useState("");
  const { user, logout } = useAuth();

  // Re-fetch patients whenever search or filter changes
  useEffect(() => {
    const params = new URLSearchParams();
    if (search) params.append("search", search);
    if (typeFilter) params.append("type", typeFilter);

    axiosClient
      .get(`/api/patients?${params.toString()}`)
      .then((res) => setPatients(res.data));
  }, [search, typeFilter]);

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Patients</h1>
        <div>
          <span>
            {user?.email} ({user?.role})
          </span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>
      <div style={{ display: "flex", gap: 12, marginBottom: 16 }}>
        <input
          type="text"
          placeholder="Search by name or patient ID..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          style={{ padding: 8, flex: 1 }}
        />
        <select
          value={typeFilter}
          onChange={(e) => setTypeFilter(e.target.value)}
          style={{ padding: 8 }}
        >
          <option value="">All Types</option>
          <option value="Outpatient">Outpatient</option>
          <option value="Inpatient">Inpatient</option>
        </select>
      </div>
      {/* Action buttons: register new patient and admin dashboard */}
      <div style={{ display: "flex", gap: 12, marginBottom: 16 }}>
        <Link to="/patients/new">
          <button style={{ padding: "8px 16px" }}>Register New Patient</button>
        </Link>
        {user?.role === "Admin" && (
          <Link to="/admin">
            <button style={{ padding: "8px 16px" }}>Admin Dashboard</button>
          </Link>
        )}
      </div>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Patient ID</th>
            <th style={thStyle}>Name</th>
            <th style={thStyle}>Gender</th>
            <th style={thStyle}>Phone</th>
            <th style={thStyle}>Type</th>
            <th style={thStyle}>Registered</th>
          </tr>
        </thead>
        <tbody>
          {/* Patient ID and name are now clickable Links */}
          {patients.map((p) => (
            <tr key={p.id}>
              <td style={tdStyle}>
                <Link to={`/patients/${p.id}`}>{p.patientUniqueId}</Link>
              </td>
              <td style={tdStyle}>
                <Link to={`/patients/${p.id}`}>
                  {p.firstName} {p.lastName}
                </Link>
              </td>
              <td style={tdStyle}>{p.gender}</td>
              <td style={tdStyle}>{p.phoneNumber}</td>
              <td style={tdStyle}>{p.patientType}</td>
              <td style={tdStyle}>
                {new Date(p.registeredAt).toLocaleDateString()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

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

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

What changed in PatientsPage?

  • The Patient ID and Name columns are now wrapped in Link components that navigate to /patients/{id}. Clicking a patient row takes users to the Patient Details page.
  • An Admin Dashboard button is conditionally rendered only when user?.role === "Admin". Other roles will not see this button.
  • Save PatientsPage.tsx.
  • Switch to your browser and refresh the patients page.
  • Log in as admin@korutla.hospital with password Admin@123 if you are not already signed in.

You should see the Admin Dashboard button next to Register New Patient. Patient IDs and names should appear as clickable blue links.

Don't see the Admin Dashboard button?

  • Make sure you are logged in as admin@korutla.hospital. The button only appears for users with the Admin role.
  • If the page shows a blank screen or errors in the browser console, the InvoicePage import in App.tsx may be causing a build failure. Check the terminal running your frontend dev server for error messages.
  • If the dev server shows an error about InvoicePage not being found, create a temporary placeholder file at hospital-frontend/src/pages/InvoicePage.tsx with a minimal export. You will replace it with the full implementation in the next step.

Help me debug my hospital frontend routing and admin button issue.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all four files before moving on.

ⓧ I'd like to double check the full code

Here is the full code for each file you created or updated in this step.

import { useEffect, useState, FormEvent } from "react";
import { Link } from "react-router";
import { useAuth } from "../context/AuthContext";
import axiosClient from "../api/axiosClient";

interface User {
  id: string;
  email: string;
  role: string;
}

export default function AdminDashboardPage() {
  const [users, setUsers] = useState<User[]>([]);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [role, setRole] = useState("Doctor");
  const [message, setMessage] = useState("");
  const [error, setError] = useState("");
  const { user, logout } = useAuth();

  const fetchUsers = () => {
    axiosClient.get("/api/admin/users").then((res) => setUsers(res.data));
  };

  useEffect(() => {
    fetchUsers();
  }, []);

  const handleCreate = async (e: FormEvent) => {
    e.preventDefault();
    setMessage("");
    setError("");
    try {
      await axiosClient.post("/api/auth/register", { email, password, role });
      setMessage(`User ${email} created with role ${role}.`);
      setEmail("");
      setPassword("");
      fetchUsers();
    } catch {
      setError("Failed to create user. Check inputs and try again.");
    }
  };

  const handleDelete = async (id: string, userEmail: string) => {
    if (!confirm(`Delete user ${userEmail}?`)) return;
    try {
      await axiosClient.delete(`/api/admin/users/${id}`);
      fetchUsers();
    } catch {
      setError("Failed to delete user.");
    }
  };

  return (
    <div style={{ padding: 20 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
        <h1>Admin Dashboard</h1>
        <div>
          <span>{user?.email} ({user?.role})</span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>

      <Link to="/patients">Back to Patients</Link>

      <h2 style={{ marginTop: 20 }}>Create Staff Account</h2>
      {message && <p style={{ color: "green" }}>{message}</p>}
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleCreate} style={{ marginBottom: 24, maxWidth: 400 }}>
        <div style={{ marginBottom: 12 }}>
          <label>Email</label><br />
          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Password</label><br />
          <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: "100%", padding: 8 }} />
        </div>
        <div style={{ marginBottom: 12 }}>
          <label>Role</label><br />
          <select value={role} onChange={(e) => setRole(e.target.value)} style={{ width: "100%", padding: 8 }}>
            <option value="Doctor">Doctor</option>
            <option value="Receptionist">Receptionist</option>
            <option value="Billing">Billing</option>
          </select>
        </div>
        <button type="submit" style={{ padding: "8px 24px" }}>Create User</button>
      </form>

      <h2>All Users</h2>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Email</th>
            <th style={thStyle}>Role</th>
            <th style={thStyle}>Actions</th>
          </tr>
        </thead>
        <tbody>
          {users.map((u) => (
            <tr key={u.id}>
              <td style={tdStyle}>{u.email}</td>
              <td style={tdStyle}>{u.role}</td>
              <td style={tdStyle}>
                {u.email !== "admin@korutla.hospital" && (
                  <button onClick={() => handleDelete(u.id, u.email)} style={{ color: "red" }}>Delete</button>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

const thStyle = { borderBottom: "2px solid #ccc", textAlign: "left" as const, padding: 8 };
const tdStyle = { borderBottom: "1px solid #eee", padding: 8 };
import { useEffect, useState, FormEvent } from "react";
import { useParams, Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  address: string;
  patientType: string;
  registeredAt: string;
}

interface BillItem {
  id: number;
  category: string;
  description: string;
  quantity: number;
  unitPrice: number;
  totalPrice: number;
}

interface Bill {
  id: number;
  billNumber: string;
  createdAt: string;
  subTotal: number;
  taxRate: number;
  taxAmount: number;
  totalAmount: number;
  paymentStatus: string;
  amountPaid: number;
  notes: string;
  items: BillItem[];
}

interface NewItem {
  category: number;
  description: string;
  quantity: number;
  unitPrice: number;
}

export default function PatientDetailPage() {
  const { id } = useParams();
  const { user, logout } = useAuth();
  const [patient, setPatient] = useState<Patient | null>(null);
  const [bills, setBills] = useState<Bill[]>([]);
  const [showForm, setShowForm] = useState(false);
  const [taxRate, setTaxRate] = useState(0);
  const [notes, setNotes] = useState("");
  const [items, setItems] = useState<NewItem[]>([
    { category: 0, description: "", quantity: 1, unitPrice: 0 },
  ]);
  const [error, setError] = useState("");

  useEffect(() => {
    axiosClient.get(`/api/patients/${id}`).then((res) => setPatient(res.data));
    axiosClient.get(`/api/billing/patient/${id}`).then((res) => setBills(res.data));
  }, [id]);

  const addItem = () => {
    setItems([...items, { category: 0, description: "", quantity: 1, unitPrice: 0 }]);
  };

  const removeItem = (index: number) => {
    setItems(items.filter((_, i) => i !== index));
  };

  const updateItem = (index: number, field: string, value: string | number) => {
    const updated = [...items];
    updated[index] = { ...updated[index], [field]: value };
    setItems(updated);
  };

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    try {
      await axiosClient.post(`/api/billing/${id}`, { taxRate, notes, items });
      setShowForm(false);
      setItems([{ category: 0, description: "", quantity: 1, unitPrice: 0 }]);
      setTaxRate(0);
      setNotes("");
      axiosClient.get(`/api/billing/patient/${id}`).then((res) => setBills(res.data));
    } catch {
      setError("Failed to create bill. Check your inputs.");
    }
  };

  if (!patient) return <p style={{ padding: 20 }}>Loading...</p>;

  return (
    <div style={{ padding: 20 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
        <h1>Patient Details</h1>
        <div>
          <span>{user?.email} ({user?.role})</span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>

      <div style={{ marginBottom: 24, padding: 16, border: "1px solid #ccc" }}>
        <p>Patient ID: {patient.patientUniqueId}</p>
        <p>Name: {patient.firstName} {patient.lastName}</p>
        <p>Gender: {patient.gender}</p>
        <p>Date of Birth: {patient.dateOfBirth}</p>
        <p>Phone: {patient.phoneNumber}</p>
        <p>Address: {patient.address}</p>
        <p>Type: {patient.patientType}</p>
        <p>Registered: {new Date(patient.registeredAt).toLocaleDateString()}</p>
      </div>

      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
        <h2>Billing History</h2>
        <button onClick={() => setShowForm(!showForm)} style={{ padding: "8px 16px" }}>
          {showForm ? "Cancel" : "Create New Bill"}
        </button>
      </div>

      {showForm && (
        <form onSubmit={handleSubmit} style={{ marginBottom: 24, padding: 16, border: "1px solid #ccc" }}>
          <h3>New Bill</h3>
          {error && <p style={{ color: "red" }}>{error}</p>}
          {items.map((item, index) => (
            <div key={index} style={{ display: "flex", gap: 8, marginBottom: 8, alignItems: "end" }}>
              <div>
                <label>Category</label><br />
                <select value={item.category} onChange={(e) => updateItem(index, "category", parseInt(e.target.value))} style={{ padding: 6 }}>
                  <option value={0}>Consultation</option>
                  <option value={1}>Lab</option>
                  <option value={2}>Medicine</option>
                  <option value={3}>Room</option>
                  <option value={4}>Other</option>
                </select>
              </div>
              <div style={{ flex: 1 }}>
                <label>Description</label><br />
                <input value={item.description} onChange={(e) => updateItem(index, "description", e.target.value)} required style={{ width: "100%", padding: 6 }} />
              </div>
              <div style={{ width: 60 }}>
                <label>Qty</label><br />
                <input type="number" value={item.quantity} onChange={(e) => updateItem(index, "quantity", parseInt(e.target.value) || 1)} min={1} style={{ width: "100%", padding: 6 }} />
              </div>
              <div style={{ width: 100 }}>
                <label>Unit Price</label><br />
                <input type="number" value={item.unitPrice} onChange={(e) => updateItem(index, "unitPrice", parseFloat(e.target.value) || 0)} min={0} step="0.01" required style={{ width: "100%", padding: 6 }} />
              </div>
              <button type="button" onClick={() => removeItem(index)} style={{ color: "red", padding: 6 }}>X</button>
            </div>
          ))}
          <button type="button" onClick={addItem} style={{ marginBottom: 12, padding: "4px 12px" }}>+ Add Item</button>
          <div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
            <div>
              <label>Tax Rate (%)</label><br />
              <input type="number" value={taxRate} onChange={(e) => setTaxRate(parseFloat(e.target.value) || 0)} min={0} step="0.01" style={{ padding: 6 }} />
            </div>
            <div style={{ flex: 1 }}>
              <label>Notes</label><br />
              <input value={notes} onChange={(e) => setNotes(e.target.value)} style={{ width: "100%", padding: 6 }} />
            </div>
          </div>
          <button type="submit" style={{ padding: "8px 24px" }}>Submit Bill</button>
        </form>
      )}

      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Bill #</th>
            <th style={thStyle}>Date</th>
            <th style={thStyle}>Total</th>
            <th style={thStyle}>Status</th>
            <th style={thStyle}>Actions</th>
          </tr>
        </thead>
        <tbody>
          {bills.map((b) => (
            <tr key={b.id}>
              <td style={tdStyle}>{b.billNumber}</td>
              <td style={tdStyle}>{new Date(b.createdAt).toLocaleDateString()}</td>
              <td style={tdStyle}>{b.totalAmount.toFixed(2)}</td>
              <td style={tdStyle}>{b.paymentStatus}</td>
              <td style={tdStyle}>
                <Link to={`/bills/${b.id}/invoice`}>View Invoice</Link>
              </td>
            </tr>
          ))}
          {bills.length === 0 && (
            <tr><td style={tdStyle} colSpan={5}>No bills yet.</td></tr>
          )}
        </tbody>
      </table>

      <div style={{ marginTop: 16 }}>
        <Link to="/patients">Back to Patients</Link>
      </div>
    </div>
  );
}

const thStyle = { borderBottom: "2px solid #ccc", textAlign: "left" as const, padding: 8 };
const tdStyle = { borderBottom: "1px solid #eee", padding: 8 };
import { BrowserRouter, Routes, Route, Navigate } from "react-router";
import { AuthProvider, useAuth } from "./context/AuthContext";
import LoginPage from "./pages/LoginPage";
import PatientsPage from "./pages/PatientsPage";
import RegisterPatientPage from "./pages/RegisterPatientPage";
import PatientDetailPage from "./pages/PatientDetailPage";
import InvoicePage from "./pages/InvoicePage";
import AdminDashboardPage from "./pages/AdminDashboardPage";
import { ReactNode } from "react";

function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  return children;
}

function AdminRoute({ children }: { children: ReactNode }) {
  const { user } = useAuth();
  if (!user) {
    return <Navigate to="/login" />;
  }
  if (user.role !== "Admin") {
    return <Navigate to="/patients" />;
  }
  return children;
}

function AppRoutes() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route
        path="/patients"
        element={
          <ProtectedRoute>
            <PatientsPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/new"
        element={
          <ProtectedRoute>
            <RegisterPatientPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/patients/:id"
        element={
          <ProtectedRoute>
            <PatientDetailPage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/bills/:billId/invoice"
        element={
          <ProtectedRoute>
            <InvoicePage />
          </ProtectedRoute>
        }
      />
      <Route
        path="/admin"
        element={
          <AdminRoute>
            <AdminDashboardPage />
          </AdminRoute>
        }
      />
      <Route path="*" element={<Navigate to="/login" />} />
    </Routes>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <AuthProvider>
        <AppRoutes />
      </AuthProvider>
    </BrowserRouter>
  );
}
import { useEffect, useState } from "react";
import { Link } from "react-router";
import axiosClient from "../api/axiosClient";
import { useAuth } from "../context/AuthContext";

interface Patient {
  id: number;
  patientUniqueId: string;
  firstName: string;
  lastName: string;
  dateOfBirth: string;
  gender: string;
  phoneNumber: string;
  patientType: string;
  registeredAt: string;
}

export default function PatientsPage() {
  const [patients, setPatients] = useState<Patient[]>([]);
  const [search, setSearch] = useState("");
  const [typeFilter, setTypeFilter] = useState("");
  const { user, logout } = useAuth();

  useEffect(() => {
    const params = new URLSearchParams();
    if (search) params.append("search", search);
    if (typeFilter) params.append("type", typeFilter);

    axiosClient
      .get(`/api/patients?${params.toString()}`)
      .then((res) => setPatients(res.data));
  }, [search, typeFilter]);

  return (
    <div style={{ padding: 20 }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: 20,
        }}
      >
        <h1>Patients</h1>
        <div>
          <span>
            {user?.email} ({user?.role})
          </span>{" "}
          <button onClick={logout}>Logout</button>
        </div>
      </div>
      <div style={{ display: "flex", gap: 12, marginBottom: 16 }}>
        <input
          type="text"
          placeholder="Search by name or patient ID..."
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          style={{ padding: 8, flex: 1 }}
        />
        <select
          value={typeFilter}
          onChange={(e) => setTypeFilter(e.target.value)}
          style={{ padding: 8 }}
        >
          <option value="">All Types</option>
          <option value="Outpatient">Outpatient</option>
          <option value="Inpatient">Inpatient</option>
        </select>
      </div>
      <div style={{ display: "flex", gap: 12, marginBottom: 16 }}>
        <Link to="/patients/new">
          <button style={{ padding: "8px 16px" }}>Register New Patient</button>
        </Link>
        {user?.role === "Admin" && (
          <Link to="/admin">
            <button style={{ padding: "8px 16px" }}>Admin Dashboard</button>
          </Link>
        )}
      </div>
      <table style={{ width: "100%", borderCollapse: "collapse" }}>
        <thead>
          <tr>
            <th style={thStyle}>Patient ID</th>
            <th style={thStyle}>Name</th>
            <th style={thStyle}>Gender</th>
            <th style={thStyle}>Phone</th>
            <th style={thStyle}>Type</th>
            <th style={thStyle}>Registered</th>
          </tr>
        </thead>
        <tbody>
          {patients.map((p) => (
            <tr key={p.id}>
              <td style={tdStyle}>
                <Link to={`/patients/${p.id}`}>{p.patientUniqueId}</Link>
              </td>
              <td style={tdStyle}>
                <Link to={`/patients/${p.id}`}>
                  {p.firstName} {p.lastName}
                </Link>
              </td>
              <td style={tdStyle}>{p.gender}</td>
              <td style={tdStyle}>{p.phoneNumber}</td>
              <td style={tdStyle}>{p.patientType}</td>
              <td style={tdStyle}>
                {new Date(p.registeredAt).toLocaleDateString()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

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

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

Now test the full flow from the patients list through to bill creation.

  • Click the Admin Dashboard button on the Patients page.
  • In the Create Staff Account form, enter an email like receptionist@korutla.hospital.
  • Enter a password like Test@1234.
  • Select Receptionist from the Role dropdown.
  • Click Create User.

You should see a green success message and the new user should appear in the users table below.

  • Click Back to Patients.
  • Click any patient's name or ID to navigate to their details page.
  • Click Create New Bill to reveal the billing form.
  • Fill in a description, set a quantity and unit price for the first item.
  • Click + Add Item to add a second line item and fill in its details.
  • Click Submit Bill.

The form should close and a new row should appear in the Billing History table with the bill number, date, total, and status.

Your Admin Dashboard and Patient Details pages are wired up and working. Next up, you will build the printable invoice page so staff can print or save bills as PDFs directly from the browser.

Build the Printable Invoice Page

Your Admin Dashboard lets administrators manage staff, and the Patient Details page lets staff create itemized bills. The last piece of the puzzle is giving staff a way to print those bills as professional invoices.

In this step, you will install a printing library called react-to-print, then build an invoice page that renders a clean, print-friendly layout. Staff can print or save it as a PDF directly from the browser.

In this step, get ready to:
  • Install the react-to-print package.
  • Build a printable invoice page with hospital header, patient info, itemized charges, and totals.
  • Test the full end-to-end billing and printing flow.
Install react-to-print

The invoice page needs to trigger the browser's native print dialog so staff can print or save as PDF. The react-to-print library handles this with a simple ref-based API.

Why react-to-print instead of react-to-pdf?

react-to-print uses the browser's built-in print dialog, which produces high-quality vectorized text output. Libraries like react-to-pdf use screenshot-based rendering that produces lower quality results for text-heavy documents like invoices.

react-to-print also has zero dependencies and integrates cleanly with React's ref system. You point it at a div and it handles the rest.

  • Switch to the terminal running your hospital-frontend project. If you are not already in the hospital-frontend directory, navigate there by running:
cd ~/Desktop/korutla-hospital/hospital-frontend
  • Install the react-to-print package by running:
npm install react-to-print
  • You should see a message confirming the package was added to your dependencies.

Install failed?

  • Make sure you are inside the hospital-frontend directory, not the root korutla-hospital folder or the HospitalApi directory.
  • Check that your Vite dev server is still running. If it stopped, restart it after the install completes.
  • Help me troubleshoot my npm install for react-to-print in my hospital-frontend React project.
Create the invoice page component

The invoice page fetches a bill's data from the API and renders it inside a ref-targeted div. Everything inside that div becomes the printable content. Buttons like "Print" and "Back" live outside the div so they do not appear on the printed invoice.

  • Create a new file called InvoicePage.tsx inside hospital-frontend/src/pages/.
  • Add the imports and TypeScript interfaces by copying the following code into the file:
import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router";
import { useReactToPrint } from "react-to-print";
import axiosClient from "../api/axiosClient";

// Shape of a single line item on the invoice
interface BillItem {
  id: number;
  category: string;
  description: string;
  quantity: number;
  unitPrice: number;
  totalPrice: number;
}

// Full bill data returned by GET /api/billing/{billId}
interface Bill {
  id: number;
  billNumber: string;
  patientName: string;
  patientUniqueId: string;
  createdAt: string;
  subTotal: number;
  taxRate: number;
  taxAmount: number;
  totalAmount: number;
  paymentStatus: string;
  amountPaid: number;
  notes: string;
  items: BillItem[];
}

What do these interfaces do?

  • BillItem describes each row in the invoice table: category, description, quantity, unit price, and computed total price.
  • Bill describes the full bill response from your API, including the bill number, patient details, all line items, and financial totals.
  • These interfaces match the shape of BillResponseDto and BillItemResponseDto that your API returns as JSON.
  • Below the interfaces, add the full InvoicePage component and style constants:
export default function InvoicePage() {
  // Extract the billId from the URL (e.g. /bills/3/invoice)
  const { billId } = useParams();
  const [bill, setBill] = useState<Bill | null>(null);

  // Ref that points to the printable content area
  const contentRef = useRef<HTMLDivElement>(null);

  // useReactToPrint returns a function that triggers the browser print dialog
  const reactToPrintFn = useReactToPrint({
    contentRef,
    documentTitle: () => `Invoice_${bill?.billNumber ?? "unknown"}`,
  });

  // Fetch the bill data when the component mounts
  useEffect(() => {
    axiosClient.get(`/api/billing/${billId}`).then((res) => setBill(res.data));
  }, [billId]);

  // Show a loading message while the API call completes
  if (!bill) return <p style={{ padding: 20 }}>Loading invoice...</p>;

  return (
    <div style={{ padding: 20 }}>
      {/* Buttons live OUTSIDE the ref div so they don't appear in print */}
      <div style={{ marginBottom: 16 }}>
        <button onClick={() => reactToPrintFn()} style={{ padding: "8px 24px", marginRight: 8 }}>
          Print / Save as PDF
        </button>
        <button onClick={() => window.history.back()} style={{ padding: "8px 24px" }}>
          Back
        </button>
      </div>

      {/* Everything inside this div is what gets printed */}
      <div ref={contentRef} style={{ padding: 40, maxWidth: 800, margin: "0 auto", fontFamily: "Arial, sans-serif" }}>
        {/* Hospital header */}
        <div style={{ textAlign: "center", marginBottom: 24 }}>
          <h1 style={{ margin: 0 }}>Korutla Hospital</h1>
          <p style={{ margin: 4, color: "#666" }}>Korutla, Jagtial District, Telangana</p>
          <p style={{ margin: 4, color: "#666" }}>Phone: +91-XXXX-XXXXXX</p>
        </div>

        <hr />

        {/* Patient details and invoice metadata side by side */}
        <div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, marginBottom: 16 }}>
          <div>
            <p><strong>Patient:</strong> {bill.patientName}</p>
            <p><strong>Patient ID:</strong> {bill.patientUniqueId}</p>
          </div>
          <div style={{ textAlign: "right" }}>
            <p><strong>Invoice #:</strong> {bill.billNumber}</p>
            <p><strong>Date:</strong> {new Date(bill.createdAt).toLocaleDateString()}</p>
            <p><strong>Status:</strong> {bill.paymentStatus}</p>
          </div>
        </div>

        {/* Itemized charges table */}
        <table style={{ width: "100%", borderCollapse: "collapse", marginBottom: 16 }}>
          <thead>
            <tr style={{ backgroundColor: "#f5f5f5" }}>
              <th style={invoiceThStyle}>#</th>
              <th style={invoiceThStyle}>Category</th>
              <th style={invoiceThStyle}>Description</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Qty</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Unit Price</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Total</th>
            </tr>
          </thead>
          <tbody>
            {bill.items.map((item, index) => (
              <tr key={item.id}>
                <td style={invoiceTdStyle}>{index + 1}</td>
                <td style={invoiceTdStyle}>{item.category}</td>
                <td style={invoiceTdStyle}>{item.description}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.quantity}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.unitPrice.toFixed(2)}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.totalPrice.toFixed(2)}</td>
              </tr>
            ))}
          </tbody>
        </table>

        {/* Totals summary aligned to the right */}
        <div style={{ display: "flex", justifyContent: "flex-end" }}>
          <div style={{ width: 250 }}>
            <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
              <span>Subtotal:</span>
              <span>{bill.subTotal.toFixed(2)}</span>
            </div>
            {bill.taxRate > 0 && (
              <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
                <span>Tax ({bill.taxRate}%):</span>
                <span>{bill.taxAmount.toFixed(2)}</span>
              </div>
            )}
            <div style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderTop: "2px solid #333", fontWeight: "bold", fontSize: 18 }}>
              <span>Total:</span>
              <span>{bill.totalAmount.toFixed(2)}</span>
            </div>
          </div>
        </div>

        {/* Optional notes section */}
        {bill.notes && (
          <div style={{ marginTop: 16 }}>
            <p><strong>Notes:</strong> {bill.notes}</p>
          </div>
        )}

        {/* Footer */}
        <div style={{ marginTop: 40, textAlign: "center", color: "#999", fontSize: 12 }}>
          <p>Thank you for choosing Korutla Hospital</p>
          <p>Generated on {new Date().toLocaleString()}</p>
        </div>
      </div>
    </div>
  );
}

// Shared styles for the invoice table
const invoiceThStyle = { border: "1px solid #ddd", padding: 8, textAlign: "left" as const };
const invoiceTdStyle = { border: "1px solid #ddd", padding: 8 };

What does this component do?

  • contentRef is a React ref attached to the div that wraps all printable content. The useReactToPrint hook uses this ref to know which part of the page to print.
  • documentTitle is a callback that returns a dynamic filename like Invoice_KRL-B-260001. This becomes the default filename when the user saves as PDF.
  • The "Print / Save as PDF" and "Back" buttons sit outside the contentRef div. This means they are visible on screen but do not appear in the printed output.
  • The tax row only renders when bill.taxRate > 0, keeping tax-free invoices clean.
  • Save InvoicePage.tsx.
  • Your Vite dev server should pick up the new file automatically. If you see an error in the terminal, check for any typos in your imports or JSX.

Seeing a compile error?

  • Make sure your import says import { useReactToPrint } from "react-to-print" with curly braces. It is a named export, not a default export.
  • Confirm react-to-print installed successfully by checking your package.json for a react-to-print entry.
  • Help me debug a compile error in my InvoicePage.tsx using react-to-print and TypeScript.

Ready to verify your full file matches what is expected?

✔️ Awesome, I've got everything!

Great. Double check you have saved InvoicePage.tsx before moving on.

ⓧ I'd like to double check the full code

Here is the complete InvoicePage.tsx file. Compare it with yours and fix any differences.

import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router";
import { useReactToPrint } from "react-to-print";
import axiosClient from "../api/axiosClient";

interface BillItem {
  id: number;
  category: string;
  description: string;
  quantity: number;
  unitPrice: number;
  totalPrice: number;
}

interface Bill {
  id: number;
  billNumber: string;
  patientName: string;
  patientUniqueId: string;
  createdAt: string;
  subTotal: number;
  taxRate: number;
  taxAmount: number;
  totalAmount: number;
  paymentStatus: string;
  amountPaid: number;
  notes: string;
  items: BillItem[];
}

export default function InvoicePage() {
  const { billId } = useParams();
  const [bill, setBill] = useState<Bill | null>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const reactToPrintFn = useReactToPrint({
    contentRef,
    documentTitle: () => `Invoice_${bill?.billNumber ?? "unknown"}`,
  });

  useEffect(() => {
    axiosClient.get(`/api/billing/${billId}`).then((res) => setBill(res.data));
  }, [billId]);

  if (!bill) return <p style={{ padding: 20 }}>Loading invoice...</p>;

  return (
    <div style={{ padding: 20 }}>
      <div style={{ marginBottom: 16 }}>
        <button onClick={() => reactToPrintFn()} style={{ padding: "8px 24px", marginRight: 8 }}>
          Print / Save as PDF
        </button>
        <button onClick={() => window.history.back()} style={{ padding: "8px 24px" }}>
          Back
        </button>
      </div>

      <div ref={contentRef} style={{ padding: 40, maxWidth: 800, margin: "0 auto", fontFamily: "Arial, sans-serif" }}>
        <div style={{ textAlign: "center", marginBottom: 24 }}>
          <h1 style={{ margin: 0 }}>Korutla Hospital</h1>
          <p style={{ margin: 4, color: "#666" }}>Korutla, Jagtial District, Telangana</p>
          <p style={{ margin: 4, color: "#666" }}>Phone: +91-XXXX-XXXXXX</p>
        </div>

        <hr />

        <div style={{ display: "flex", justifyContent: "space-between", marginTop: 16, marginBottom: 16 }}>
          <div>
            <p><strong>Patient:</strong> {bill.patientName}</p>
            <p><strong>Patient ID:</strong> {bill.patientUniqueId}</p>
          </div>
          <div style={{ textAlign: "right" }}>
            <p><strong>Invoice #:</strong> {bill.billNumber}</p>
            <p><strong>Date:</strong> {new Date(bill.createdAt).toLocaleDateString()}</p>
            <p><strong>Status:</strong> {bill.paymentStatus}</p>
          </div>
        </div>

        <table style={{ width: "100%", borderCollapse: "collapse", marginBottom: 16 }}>
          <thead>
            <tr style={{ backgroundColor: "#f5f5f5" }}>
              <th style={invoiceThStyle}>#</th>
              <th style={invoiceThStyle}>Category</th>
              <th style={invoiceThStyle}>Description</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Qty</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Unit Price</th>
              <th style={{ ...invoiceThStyle, textAlign: "right" }}>Total</th>
            </tr>
          </thead>
          <tbody>
            {bill.items.map((item, index) => (
              <tr key={item.id}>
                <td style={invoiceTdStyle}>{index + 1}</td>
                <td style={invoiceTdStyle}>{item.category}</td>
                <td style={invoiceTdStyle}>{item.description}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.quantity}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.unitPrice.toFixed(2)}</td>
                <td style={{ ...invoiceTdStyle, textAlign: "right" }}>{item.totalPrice.toFixed(2)}</td>
              </tr>
            ))}
          </tbody>
        </table>

        <div style={{ display: "flex", justifyContent: "flex-end" }}>
          <div style={{ width: 250 }}>
            <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
              <span>Subtotal:</span>
              <span>{bill.subTotal.toFixed(2)}</span>
            </div>
            {bill.taxRate > 0 && (
              <div style={{ display: "flex", justifyContent: "space-between", padding: "4px 0" }}>
                <span>Tax ({bill.taxRate}%):</span>
                <span>{bill.taxAmount.toFixed(2)}</span>
              </div>
            )}
            <div style={{ display: "flex", justifyContent: "space-between", padding: "8px 0", borderTop: "2px solid #333", fontWeight: "bold", fontSize: 18 }}>
              <span>Total:</span>
              <span>{bill.totalAmount.toFixed(2)}</span>
            </div>
          </div>
        </div>

        {bill.notes && (
          <div style={{ marginTop: 16 }}>
            <p><strong>Notes:</strong> {bill.notes}</p>
          </div>
        )}

        <div style={{ marginTop: 40, textAlign: "center", color: "#999", fontSize: 12 }}>
          <p>Thank you for choosing Korutla Hospital</p>
          <p>Generated on {new Date().toLocaleString()}</p>
        </div>
      </div>
    </div>
  );
}

const invoiceThStyle = { border: "1px solid #ddd", padding: 8, textAlign: "left" as const };
const invoiceTdStyle = { border: "1px solid #ddd", padding: 8 };
Test the full end-to-end flow

Time to walk through the entire billing and printing workflow from login to printed invoice. This confirms every piece you have built across the last three steps works together.

  • Make sure both your HospitalApi backend and hospital-frontend dev server are running.
  • Open your browser and navigate to http://localhost:5173.
  • Log in with the admin credentials: email admin@korutla.hospital and password Admin@123.
  • On the Patients page, click a patient's name or ID to open their detail page.
  • Click Create New Bill.
  • Add at least two line items using the category dropdown, description, quantity, and unit price fields.
  • Enter a tax rate (for example, 5) and an optional note.
  • Click Submit Bill.
  • The new bill should appear in the billing history table below.
  • Click View Invoice next to the bill you just created.
  • You should see the invoice page with the Korutla Hospital header, patient info, your itemized charges, and the totals summary.
  • Click Print / Save as PDF.
  • The browser's print dialog should open. Verify that the print preview shows a clean invoice without the "Print / Save as PDF" or "Back" buttons.

What should I see in the print preview?

The print preview should show the hospital name and address centered at the top, patient name and ID on the left, invoice number and date on the right, an itemized charges table, subtotal/tax/total summary, any notes, and a footer. No buttons or navigation elements should appear.

Print dialog not opening?

  • Make sure you are calling reactToPrintFn() with parentheses inside an arrow function: onClick={() => reactToPrintFn()}. Passing the function directly without wrapping it can cause issues.
  • Check that the contentRef is initialized with useRef<HTMLDivElement>(null). A ref initialized without null can result in a "nothing to print" warning.
  • Confirm the ref={contentRef} attribute is on the inner div wrapping the invoice content, not on the outer page div.
  • Help me debug why the print dialog is not opening on my InvoicePage.

Your hospital system now has a complete billing workflow from patient selection through printed invoice. In the Secret Mission, you will add payment recording so staff can track partial and full payments against bills.

Secret mission

Add Payment Recording

Right now every bill is permanently Unpaid. Can you add the ability for staff to record payments against a bill, tracking partial and full payment status? You will need to add a new API endpoint, handle payment arithmetic server-side, and build a payment UI that updates the bill status in real time.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The korutla-hospital project folder on your Desktop containing HospitalApi/ and hospital-frontend/.
  • The korutla_hospital PostgreSQL database with Patients, Bills, and BillItems tables.
  • Running dotnet run and npm run dev development server processes.

Keep everything running

No action needed. Choose this if you're still actively building or plan to continue the project right away.

  • Your dotnet run and npm run dev processes stay running in their terminal windows.
  • Your korutla_hospital PostgreSQL database stays available on your machine.
  • Your project files remain in ~/Desktop/korutla-hospital/.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and database stay intact for when you return.

  • Switch to the terminal window running the backend API and press Ctrl+C to stop the dotnet run process.
  • Switch to the terminal window running the frontend and press Ctrl+C to stop the npm run dev process.
  • Your project files and database will be waiting when you come back. Restart by running dotnet run in the HospitalApi/ folder and npm run dev in the hospital-frontend/ folder.

Delete - I don't want to use this again

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

  • Stop the backend API by switching to its terminal window and pressing Ctrl+C.
  • Stop the frontend dev server by switching to its terminal window and pressing Ctrl+C.
  • Drop the PostgreSQL database by running this command:
sudo -u postgres psql -c "DROP DATABASE korutla_hospital;"
  • Delete the project folder by running these commands:

macOS

cd ~/Desktop
rm -rf korutla-hospital

Windows

cd ~\Desktop
rmdir /s /q korutla-hospital
  • Confirm the folder is gone by running:
ls ~/Desktop
  • You should no longer see korutla-hospital in the output.

Nice Work!

Nice Work!

Nice work! You just extended a full-stack hospital management system with billing, administration, and professional invoice generation.

You've learned how to:

  • Extended a PostgreSQL database with Bill and BillItem entity models, configured Entity Framework Core decimal precision for currency columns, and ran a database migration that added new tables without breaking existing ones.
  • Created BillingController and AdminController with role-based authorization, enabling itemized bill creation with auto-generated bill numbers and staff management through the web UI.
  • Built three new React pages: an Admin Dashboard for managing staff, a Patient Details page with dynamic bill creation, and a printable Invoice page powered by react-to-print.
  • Secret Mission: Added payment recording with partial and full payment tracking and updated invoices to show payment status and balance due.

Ready to quiz yourself?