Add Patient Billing and Invoice Printing
Extend a hospital system with a billing form and printable invoices.
Introduction
30 Second Summary
Every hospital visit ends with a bill. Whether it's a consultation fee, room charges, or vaccine costs, patients need a clear, printed invoice they can take home.
In this project, you will add billing and invoice printing to an existing hospital management system. Staff will be able to enter charges, see a live total, and print a professional invoice straight from the browser.
What You'll Build
A billing extension for the Korutla Hospital system where staff click a Patient ID, enter charges across four categories, and print a formatted invoice through the browser's native print dialog.
By the end of this project, you'll have:
- Clickable Patient IDs in the patients table that navigate to a dedicated billing page using React Router params.
- A billing form with four charge categories (Consultation Fee, Room Charges, Vaccines, Extra) and a live-updating total.
- A printable invoice with hospital branding, patient details, itemized charges, and signature lines powered by react-to-print.
- Secret Mission: Save bills to the backend and display billing history per patient.
Are there any prerequisites?
This project builds on the Hospital System with ASP.NET Core and React NextWork project. You should have the Korutla Hospital codebase running locally with both the API and React frontend functional before starting.
Install react-to-print and Add the Billing Route
The hospital system can register and list patients, but there is no way to generate a bill for a patient visit. Hospital staff need to click on a patient, enter charges, and print an invoice.
In this step, you will install the react-to-print library, make Patient IDs in the table clickable, and wire up a new billing route in React Router.
In this step, get ready to:
- Install react-to-print in the frontend project.
- Turn Patient IDs into clickable links that navigate to a billing page.
- Register a billing route and create a placeholder page to test the navigation.
Install react-to-print
react-to-print lets you send a specific part of your page to the browser's print dialog. Instead of printing the entire page (navigation, buttons, forms), it isolates just the content you point it at.
- In your terminal, make sure you are inside the hospital-frontend directory.
- Install react-to-print by running this command:
npm install react-to-print@3.3.0
- Open package.json and confirm that react-to-print appears in the dependencies section with version 3.3.0.
Why react-to-print instead of window.print()?
Calling window.print() sends the entire page to the print dialog, including all your navigation, forms, and buttons. You would need extensive CSS @media print rules to hide everything except the invoice.
react-to-print solves this by copying only the content you point it at into a hidden iframe, then printing that iframe. You get a clean invoice with zero extra CSS work.
Install failed?
Make sure your terminal is inside the hospital-frontend folder, not the root korutla-hospital folder or the HospitalApi folder. Running npm install from the wrong directory will either fail or install the package in the wrong place.
Help me troubleshoot my react-to-print install in the hospital-frontend project.
Make Patient IDs clickable
Right now the patients table shows each Patient ID as plain text. Staff need to click a Patient ID to open that patient's billing page.
The Link component from React Router is already imported in PatientsPage.tsx. You just need to wrap the Patient ID text in a Link that points to the billing route.
- In PatientsPage.tsx, scroll down to the <tbody> section inside the patients table.
- Find the table cell that renders the Patient ID:
<td style={tdStyle}>
{p.patientUniqueId}
</td>
- Replace that <td> block with the following, wrapping the Patient ID in a Link:
<td style={tdStyle}>
<Link to={`/patients/${p.id}/billing`} style={{ color: "#0066cc" }}>
{p.patientUniqueId}
</Link>
</td>
What does this code do?
The Link component creates a client-side navigation link. When clicked, it updates the URL to /patients/{id}/billing without a full page reload.
Notice the template literal uses p.id (the database integer ID), not p.patientUniqueId (the KRL-YYNNNN string). The billing page will use this numeric ID to fetch patient data from the API.
- Save PatientsPage.tsx.
You won't see a change yet because the billing route doesn't exist. The next substep creates it.
✔️ Awesome, I've got everything!
Great. Make sure you have saved PatientsPage.tsx before moving on.
ⓧ I'd like to double check the full code
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 { user, logout } = useAuth();
useEffect(() => {
axiosClient.get("/api/patients").then((res) => setPatients(res.data));
}, []);
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>
<Link to="/patients/new">
<button style={{ marginBottom: 16, padding: "8px 16px" }}>
Register New Patient
</button>
</Link>
<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}/billing`} style={{ color: "#0066cc" }}>
{p.patientUniqueId}
</Link>
</td>
<td style={tdStyle}>
{p.firstName} {p.lastName}
</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,
};
Create the billing route and placeholder page
The Patient ID links point to /patients/:id/billing, but React Router does not know about that path yet. You need a component for it to render and a route entry in App.tsx.
- Create a new file called BillingPage.tsx inside the src/pages/ folder.
- Add the following placeholder code to the new file:
export default function BillingPage() {
return (
<div style={{ padding: 20 }}>
<h2>Billing Page (Coming Soon)</h2>
</div>
);
}
What does this code do?
This is a minimal placeholder component. It renders a heading so you can confirm the route works before building the full billing form in the next step.
- Save BillingPage.tsx.
Now you need to tell React Router about this new page. The route will sit alongside the existing patient routes in App.tsx.
- Open App.tsx in the src/ folder.
- At the top of the file, find the existing page imports. Add this import line below the RegisterPatientPage import:
import BillingPage from "./pages/BillingPage";
- Scroll down to the AppRoutes function. Find the /patients/new route block.
- Add the following route directly below the closing /> of the /patients/new route:
<Route
path="/patients/:id/billing"
element={
<ProtectedRoute>
<BillingPage />
</ProtectedRoute>
}
/>
What does this code do?
The :id in the path is a route parameter. When a user visits /patients/5/billing, React Router captures 5 as the id parameter. The billing page will read this value to fetch the correct patient.
Wrapping BillingPage in ProtectedRoute ensures only logged-in users can access the billing page, matching how the other patient routes work.
- Save App.tsx.
Seeing a compile error?
If the terminal shows an error about BillingPage not being found, double check that you saved BillingPage.tsx inside src/pages/ (not in src/ directly). The import path in App.tsx expects ./pages/BillingPage.
Help me fix a compile error after adding BillingPage to my hospital-frontend project.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both BillingPage.tsx and App.tsx.
ⓧ I'd like to double check the full code
Here is the full BillingPage.tsx placeholder:
export default function BillingPage() {
return (
<div style={{ padding: 20 }}>
<h2>Billing Page (Coming Soon)</h2>
</div>
);
}
Here is the full App.tsx with the new import and route:
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 BillingPage from "./pages/BillingPage";
import { ReactNode } from "react";
function ProtectedRoute({ children }: { children: ReactNode }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" />;
}
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/billing"
element={
<ProtectedRoute>
<BillingPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<Navigate to="/login" />} />
</Routes>
);
}
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
);
}
Test the billing route end to end
- Make sure both the API server (dotnet run in the HospitalApi folder) and the frontend dev server (npm run dev in the hospital-frontend folder) are running.
- Open the app in your browser and log in.
- Navigate to the patients list. Each Patient ID should now appear as a blue, clickable link.
- Click any Patient ID.
The browser URL should change to something like /patients/1/billing and you should see the heading "Billing Page (Coming Soon)" on a blank page.
Clicking the Patient ID does nothing?
Check that the <Link> wraps the {p.patientUniqueId} text inside the <td> tag in PatientsPage.tsx. If the text is not blue and underlined, the Link wrapper is missing or misplaced.
Also confirm that the route path in App.tsx is /patients/:id/billing (with a colon before id). Missing the colon means React Router treats it as a literal string instead of a parameter.
Help me figure out why clicking a Patient ID doesn't navigate to the billing page.
Your billing route is wired up and the Patient IDs are clickable. Next up, you will replace that placeholder with a full billing form where staff can enter charges and see a running total.
Build the Billing Form with Charge Entry
Your routing is wired up and the placeholder billing page loads when you click a Patient ID. Now it is time to turn that placeholder into a real billing form.
The billing page needs to know which patient it is billing. It will extract the patient's ID from the URL, fetch their details from the API, and present a form where hospital staff can enter charges across four categories.
In this step, get ready to:
- Fetch the patient's details from the API and display loading and error states.
- Build a charge entry table with four editable rows and a live-updating total.
Fetch the patient and handle loading states
When the billing page loads, it needs to read the patient ID from the URL and call the API to get that patient's details. While the request is in progress, the page should show a loading message. If the patient is not found, it should show an error with a way to go back.
- In VS Code, open src/pages/BillingPage.tsx.
- Select all the placeholder code and replace it with the following:
import { useEffect, useState, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";
// Shape of a patient record from the API
interface Patient {
id: number;
patientUniqueId: string;
firstName: string;
lastName: string;
dateOfBirth: string;
gender: string;
phoneNumber: string;
address: string;
patientType: string;
registeredAt: string;
}
// Each charge row has a text description and a numeric amount
interface ChargeItem {
description: string;
amount: number;
}
export default function BillingPage() {
// Extract the patient's database ID from the URL
const { id } = useParams();
const navigate = useNavigate();
// Patient data, loading flag, and error message
const [patient, setPatient] = useState<Patient | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
// Fetch the patient's details when the component mounts
useEffect(() => {
axiosClient
.get(`/api/patients/${id}`)
.then((res) => {
setPatient(res.data);
setLoading(false);
})
.catch(() => {
setError("Patient not found.");
setLoading(false);
});
}, [id]);
// Show a loading message while the API call is in progress
if (loading) return <div style={{ padding: 20 }}>Loading patient...</div>;
// Show an error with a Back button if the patient was not found
if (error)
return (
<div style={{ padding: 20 }}>
<p style={{ color: "red" }}>{error}</p>
<button
onClick={() => navigate("/patients")}
style={{ padding: "8px 16px" }}
>
Back to Patients
</button>
</div>
);
if (!patient) return null;
return (
<div style={{ padding: 20 }}>
<button
onClick={() => navigate("/patients")}
style={{ marginBottom: 16, padding: "8px 16px" }}
>
Back to Patients
</button>
<h2>
Billing - {patient.firstName} {patient.lastName} ({patient.patientUniqueId})
</h2>
</div>
);
}
What does this code do?
useParams() reads the :id segment from the URL (the same ID the patients list linked to). useNavigate() gives us a function to programmatically send the user back to the patients list.
The useEffect calls GET /api/patients/{id} through the project's existing axiosClient, which automatically attaches the JWT token. The three conditional returns handle loading, error, and null-patient states before the main JSX runs.
You may notice useRef in the imports even though we are not using it yet. You will need it in the next step for the printable invoice.
- Save BillingPage.tsx.
- Switch back to your browser and click any Patient ID in the patients list.
- You should see a heading that reads "Billing - [First] [Last] ([Patient ID])" and a Back to Patients button.
Seeing a blank page or an error?
- Check that your HospitalApi server is still running. The billing page calls GET /api/patients/{id}, so the API must be reachable.
- Make sure the import path matches exactly: import axiosClient from "../api/axiosClient". A typo here causes a module-not-found error.
- If the page shows "Patient not found." in red text, the ID in the URL does not match any patient in the database. Click Back to Patients and try a different Patient ID link.
Still stuck? Ask for help.
Add the charge entry form
Hospital staff need to enter charges across four categories: Consultation Fee, Room Charges, Vaccines, and Extra/Miscellaneous. Each category has a description and an amount, stored as separate state variables so the form stays responsive as values change.
- In BillingPage.tsx, find this line inside the component function:
const [error, setError] = useState("");
- Add the four charge state variables directly below it by copying this code:
// Four charge categories, each with a description and amount
const [consultation, setConsultation] = useState<ChargeItem>({
description: "General Consultation",
amount: 0,
});
const [roomCharges, setRoomCharges] = useState<ChargeItem>({
description: "Room Charges",
amount: 0,
});
const [vaccines, setVaccines] = useState<ChargeItem>({
description: "",
amount: 0,
});
const [extra, setExtra] = useState<ChargeItem>({
description: "",
amount: 0,
});
Why four separate state variables?
Each charge category is its own ChargeItem state object with a description string and an amount number. Consultation Fee and Room Charges come pre-filled with default descriptions because those are standard line items. Vaccines and Extra start with empty descriptions so staff can type in specifics.
Now you need a table in the JSX where staff can edit these values.
- In the same file, find the closing </h2> tag inside the return statement.
- Add the charge entry table below it by copying this code:
<div
style={{
border: "1px solid #ccc",
padding: 16,
marginBottom: 24,
borderRadius: 4,
}}
>
<h3 style={{ marginTop: 0 }}>Enter Charges</h3>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={formThStyle}>Charge Type</th>
<th style={formThStyle}>Description</th>
<th style={formThStyle}>Amount (INR)</th>
</tr>
</thead>
<tbody>
{/* Consultation Fee row */}
<tr>
<td style={formTdStyle}>Consultation Fee</td>
<td style={formTdStyle}>
<input
value={consultation.description}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={consultation.amount || ""}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
{/* Room Charges row */}
<tr>
<td style={formTdStyle}>Room Charges</td>
<td style={formTdStyle}>
<input
value={roomCharges.description}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={roomCharges.amount || ""}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
{/* Vaccines row */}
<tr>
<td style={formTdStyle}>Vaccines</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Hepatitis B, Tetanus"
value={vaccines.description}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={vaccines.amount || ""}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
{/* Extra / Miscellaneous row */}
<tr>
<td style={formTdStyle}>Extra / Miscellaneous</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Lab tests, X-ray"
value={extra.description}
onChange={(e) =>
setExtra((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={extra.amount || ""}
onChange={(e) =>
setExtra((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
</tbody>
</table>
</div>
What does this table do?
Each table row maps to one of the four charge state variables. The first column is a read-only label. The second column is an editable text input bound to the charge's description. The third column is a number input bound to the charge's amount.
The onChange handlers use the spread pattern (...prev) to update only the field that changed while keeping the other field intact. The Vaccines and Extra rows show placeholder text since their descriptions vary per patient.
- Save BillingPage.tsx.
Your browser may show errors in the terminal because the style constants (formThStyle, formTdStyle, inputStyle) are referenced but not defined yet. You will add those in the next substep.
Calculate and display the live total
The form needs a running total that updates instantly as staff type amounts. Since React re-renders whenever state changes, you can compute the total as a plain variable derived from the four amounts.
- In BillingPage.tsx, find the closing of the extra state declaration you just added:
const [extra, setExtra] = useState<ChargeItem>({
description: "",
amount: 0,
});
- Add the total calculation directly below it:
// Sum all four amounts for a live total
const total =
consultation.amount + roomCharges.amount + vaccines.amount + extra.amount;
Now add the total display inside the charges div and the style constants at the bottom of the file.
- Scroll down to the charge entry table you added in the previous substep.
- Find the closing </table> tag.
- Add the total display between </table> and the closing </div> of the charges container:
<div
style={{
textAlign: "right",
marginTop: 12,
fontSize: 18,
fontWeight: "bold",
}}
>
Total: INR {total.toFixed(2)}
</div>
- Scroll to the very bottom of the file, after the closing } of the BillingPage function.
- Add the style constants below the component by copying this code:
const formThStyle = {
borderBottom: "2px solid #ccc",
textAlign: "left" as const,
padding: 8,
};
const formTdStyle = {
borderBottom: "1px solid #eee",
padding: 8,
};
const inputStyle = {
width: "100%",
padding: 6,
boxSizing: "border-box" as const,
};
What does this code do?
The total variable is recalculated on every render. Whenever the user types a new amount, React updates that charge's state, which triggers a re-render, which recalculates the total automatically. No manual "recalculate" button needed.
total.toFixed(2) formats the number to exactly two decimal places, so the display always shows currency-style values like 1500.00.
The three style constants (formThStyle, formTdStyle, inputStyle) live outside the component so they are created once rather than on every render. This matches the inline-style pattern used throughout the existing project.
- Save BillingPage.tsx.
- Switch back to your browser and navigate to a patient's billing page by clicking any Patient ID link.
- You should see the patient's name and ID in the heading, a bordered "Enter Charges" table with four rows, and a "Total: INR 0.00" line at the bottom right of the table.
- Type 500 into the Consultation Fee amount field. The total should instantly update to 500.00.
Total not updating or table not showing?
- If the page is blank or shows an error, check the terminal running your frontend dev server. Look for a syntax error pointing to a missing bracket or parenthesis in BillingPage.tsx.
- Make sure the total variable is declared after all four charge state variables and before the useEffect. If it is above the state declarations, it will reference undefined variables.
- Verify the style constants (formThStyle, formTdStyle, inputStyle) are placed outside and below the component function, not inside it.
Help me debug my billing form.
✔️ Awesome, I've got everything!
Great. Double check you have saved BillingPage.tsx before moving on.
ⓧ I'd like to double check the full code
Here is the complete BillingPage.tsx at this point. Compare it with your file and fix any differences.
import { useEffect, useState, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import axiosClient from "../api/axiosClient";
interface Patient {
id: number;
patientUniqueId: string;
firstName: string;
lastName: string;
dateOfBirth: string;
gender: string;
phoneNumber: string;
address: string;
patientType: string;
registeredAt: string;
}
interface ChargeItem {
description: string;
amount: number;
}
export default function BillingPage() {
const { id } = useParams();
const navigate = useNavigate();
const [patient, setPatient] = useState<Patient | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [consultation, setConsultation] = useState<ChargeItem>({
description: "General Consultation",
amount: 0,
});
const [roomCharges, setRoomCharges] = useState<ChargeItem>({
description: "Room Charges",
amount: 0,
});
const [vaccines, setVaccines] = useState<ChargeItem>({
description: "",
amount: 0,
});
const [extra, setExtra] = useState<ChargeItem>({
description: "",
amount: 0,
});
const total =
consultation.amount + roomCharges.amount + vaccines.amount + extra.amount;
useEffect(() => {
axiosClient
.get(`/api/patients/${id}`)
.then((res) => {
setPatient(res.data);
setLoading(false);
})
.catch(() => {
setError("Patient not found.");
setLoading(false);
});
}, [id]);
if (loading) return <div style={{ padding: 20 }}>Loading patient...</div>;
if (error)
return (
<div style={{ padding: 20 }}>
<p style={{ color: "red" }}>{error}</p>
<button
onClick={() => navigate("/patients")}
style={{ padding: "8px 16px" }}
>
Back to Patients
</button>
</div>
);
if (!patient) return null;
return (
<div style={{ padding: 20 }}>
<button
onClick={() => navigate("/patients")}
style={{ marginBottom: 16, padding: "8px 16px" }}
>
Back to Patients
</button>
<h2>
Billing - {patient.firstName} {patient.lastName} ({patient.patientUniqueId})
</h2>
<div
style={{
border: "1px solid #ccc",
padding: 16,
marginBottom: 24,
borderRadius: 4,
}}
>
<h3 style={{ marginTop: 0 }}>Enter Charges</h3>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={formThStyle}>Charge Type</th>
<th style={formThStyle}>Description</th>
<th style={formThStyle}>Amount (INR)</th>
</tr>
</thead>
<tbody>
<tr>
<td style={formTdStyle}>Consultation Fee</td>
<td style={formTdStyle}>
<input
value={consultation.description}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={consultation.amount || ""}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Room Charges</td>
<td style={formTdStyle}>
<input
value={roomCharges.description}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={roomCharges.amount || ""}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Vaccines</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Hepatitis B, Tetanus"
value={vaccines.description}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={vaccines.amount || ""}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Extra / Miscellaneous</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Lab tests, X-ray"
value={extra.description}
onChange={(e) =>
setExtra((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={extra.amount || ""}
onChange={(e) =>
setExtra((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
</tbody>
</table>
<div
style={{
textAlign: "right",
marginTop: 12,
fontSize: 18,
fontWeight: "bold",
}}
>
Total: INR {total.toFixed(2)}
</div>
</div>
</div>
);
}
const formThStyle = {
borderBottom: "2px solid #ccc",
textAlign: "left" as const,
padding: 8,
};
const formTdStyle = {
borderBottom: "1px solid #eee",
padding: 8,
};
const inputStyle = {
width: "100%",
padding: 6,
boxSizing: "border-box" as const,
};
Your billing form is functional. Staff can look up a patient and enter charges across all four categories. Next up, you will build a printable invoice layout and wire it to the browser's print dialog so staff can print a professional hospital bill with one click.
Add the Printable Invoice and Print Button
Your billing form captures charges across four categories and displays a live total. The next piece is turning those numbers into a document that hospital staff can print and hand to a patient.
In this step, you'll build a professional invoice layout inside the billing page and wire it up to react-to-print. When staff click the Print Invoice button, only the invoice content appears in the browser's print dialog. No form fields, no navigation, no buttons.
In this step, get ready to:
- Build a printable invoice layout with hospital branding, patient details, itemized charges, and signature lines.
- Wire up react-to-print so the Print Invoice button opens the browser's native print dialog.
- Test the full end-to-end flow from charge entry to print preview.
Create the printable invoice layout
The invoice is a hidden-in-plain-sight div that sits below the charge entry form on the page. It renders a clean, serif-font document with the hospital header, patient details, an itemized charges table, a total, and signature lines. Later, react-to-print will isolate this div for printing.
- In BillingPage.tsx, find this line near the top of the component:
const navigate = useNavigate();
- Add this line directly below it to create a ref that will point to the invoice div:
const invoiceRef = useRef<HTMLDivElement>(null);
- Now scroll down to the return statement. Find the closing </div> that wraps the charges section (the one right after the Total: INR line). Add the following invoice layout below that closing </div>, but before the outer wrapper's closing </div>:
<div
ref={invoiceRef}
style={{
border: "1px solid #ccc",
padding: 32,
maxWidth: 700,
margin: "0 auto",
fontFamily: "serif",
}}
>
{/* Hospital header with name, address, and phone */}
<div style={{ textAlign: "center", marginBottom: 24 }}>
<h1 style={{ margin: 0 }}>Korutla Hospital</h1>
<p style={{ margin: "4px 0", color: "#555" }}>
Korutla, Jagtial District, Telangana
</p>
<p style={{ margin: "4px 0", color: "#555" }}>
Phone: +91-XXXXX-XXXXX
</p>
<hr />
</div>
<h2 style={{ textAlign: "center", marginBottom: 16 }}>
Patient Bill / Invoice
</h2>
{/* Patient details table showing ID, name, contact, and today's date */}
<table style={{ width: "100%", marginBottom: 16 }}>
<tbody>
<tr>
<td>
<strong>Patient ID:</strong> {patient.patientUniqueId}
</td>
<td>
<strong>Date:</strong>{" "}
{new Date().toLocaleDateString("en-IN")}
</td>
</tr>
<tr>
<td>
<strong>Name:</strong> {patient.firstName} {patient.lastName}
</td>
<td>
<strong>Gender:</strong> {patient.gender}
</td>
</tr>
<tr>
<td>
<strong>Phone:</strong> {patient.phoneNumber}
</td>
<td>
<strong>Type:</strong> {patient.patientType}
</td>
</tr>
<tr>
<td colSpan={2}>
<strong>Address:</strong> {patient.address}
</td>
</tr>
</tbody>
</table>
{/* Itemized charges table - only shows rows where amount > 0 */}
<table
style={{
width: "100%",
borderCollapse: "collapse",
marginBottom: 16,
}}
>
<thead>
<tr>
<th style={invoiceThStyle}>S.No</th>
<th style={invoiceThStyle}>Description</th>
<th style={{ ...invoiceThStyle, textAlign: "right" }}>
Amount (INR)
</th>
</tr>
</thead>
<tbody>
{[consultation, roomCharges, vaccines, extra]
.filter((item) => item.amount > 0)
.map((item, index) => (
<tr key={index}>
<td style={invoiceTdStyle}>{index + 1}</td>
<td style={invoiceTdStyle}>
{item.description || "N/A"}
</td>
<td style={{ ...invoiceTdStyle, textAlign: "right" }}>
{item.amount.toFixed(2)}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td
colSpan={2}
style={{
...invoiceTdStyle,
fontWeight: "bold",
textAlign: "right",
borderTop: "2px solid #333",
}}
>
Total
</td>
<td
style={{
...invoiceTdStyle,
fontWeight: "bold",
textAlign: "right",
borderTop: "2px solid #333",
}}
>
{total.toFixed(2)}
</td>
</tr>
</tfoot>
</table>
{/* Signature lines for authorized staff and patient */}
<div
style={{
marginTop: 40,
display: "flex",
justifyContent: "space-between",
}}
>
<div>
<p>____________________</p>
<p>Authorized Signature</p>
</div>
<div>
<p>____________________</p>
<p>Patient / Attendant</p>
</div>
</div>
<p
style={{
textAlign: "center",
marginTop: 32,
fontSize: 12,
color: "#888",
}}
>
Thank you for choosing Korutla Hospital. Get well soon!
</p>
</div>
- Scroll to the bottom of the file, below the existing inputStyle constant. Add these two style objects for the invoice tables:
// Styles for the invoice's itemized charges table
const invoiceThStyle = {
borderBottom: "2px solid #333",
textAlign: "left" as const,
padding: 8,
};
const invoiceTdStyle = {
borderBottom: "1px solid #ddd",
padding: 8,
};
What does this invoice layout do?
The invoiceRef is a React ref that gives react-to-print a handle to the invoice div. When printing, only the content inside this ref gets sent to the print dialog.
- The hospital header displays "Korutla Hospital" centered with the address and phone number, followed by an <hr /> divider.
- The patient details table pulls live data from the patient state (ID, name, gender, phone, type, address) and adds today's date.
- The itemized charges table uses .filter((item) => item.amount > 0) so only charge rows the staff actually filled in appear on the invoice.
- The tfoot row shows the total with a bold top border to visually separate it from the line items.
- Save BillingPage.tsx and switch back to your browser.
- Navigate to any patient's billing page. Below the charge entry form, you should see the invoice layout with the "Korutla Hospital" header, patient details, and signature lines.
Don't see the invoice layout?
- Check that the invoice div is placed inside the return statement's outer wrapper <div style={{ padding: 20 }}>, not outside it.
- Make sure invoiceThStyle and invoiceTdStyle are defined at the bottom of the file, outside the component function.
- If you see a red error in the browser, check for missing closing tags or brackets. The invoice div has several nested tables.
Why isn't my invoice layout rendering on the billing page?
Wire up react-to-print and the print button
The invoice layout is on the page, but clicking the browser's native print shortcut would print the entire page including the form, navigation, and buttons. The useReactToPrint hook solves this by copying only the invoice div into a hidden iframe and sending that to the print dialog.
- At the top of BillingPage.tsx, find this import line:
import axiosClient from "../api/axiosClient";
- Add this new import directly above it:
import { useReactToPrint } from "react-to-print";
- Now find the total calculation line inside the component. Add the handlePrint hook configuration directly below it, before the useEffect:
// Configure react-to-print with the invoice ref, dynamic title, and A4 page settings
const handlePrint = useReactToPrint({
contentRef: invoiceRef,
documentTitle: () =>
`Invoice_${patient?.patientUniqueId ?? "unknown"}_${new Date().toISOString().split("T")[0]}`,
pageStyle: `
@page { size: A4; margin: 20mm; }
@media print {
body { -webkit-print-color-adjust: exact; }
}
`,
});
- Finally, add the Print Invoice button in the return statement. Find the closing </div> of the charges section (the one right after the Total: INR display). Add this button between that closing </div> and the invoice div you added in the previous substep:
{/* Print button - disabled when no charges are entered */}
<button
onClick={handlePrint}
disabled={total === 0}
style={{
padding: "10px 24px",
marginBottom: 24,
fontSize: 16,
cursor: total === 0 ? "not-allowed" : "pointer",
}}
>
Print Invoice
</button>
What does useReactToPrint do?
- contentRef: invoiceRef tells the hook which DOM element to print. Only the content inside this ref appears in the print dialog.
- documentTitle is a function (not a static string) so it evaluates at print time. This means the title includes the actual patient ID and today's date, even though the patient data loads asynchronously after the hook is initialized.
- pageStyle sets the printed page to A4 size with 20mm margins. The -webkit-print-color-adjust: exact rule ensures background colors and borders print as they appear on screen.
- The button is disabled when total === 0 to prevent printing a blank invoice with no charges.
Why react-to-print instead of window.print()?
Calling window.print() sends the entire page to the print dialog. You would need extensive @media print CSS to hide the form, navigation, and buttons.
react-to-print avoids that by copying only the ref'd content into a hidden iframe and printing from there. The result is a clean invoice with zero extra CSS work.
- Save BillingPage.tsx.
- In your browser, navigate to a patient's billing page. You should see a Print Invoice button between the charges section and the invoice layout. The button should appear greyed out with a "not-allowed" cursor.
- Enter a value (e.g. 500) in the Consultation Fee amount field. The button should become clickable.
Print Invoice button not appearing?
- Make sure you added the import { useReactToPrint } from "react-to-print" line at the top of the file. A missing import causes a compile error.
- Check that the handlePrint hook is placed inside the component function, not outside it. It must be between the total calculation and the useEffect.
- Verify the button's onClick references handlePrint (not handlePrint() with parentheses, which would call it on every render).
Help me debug why my Print Invoice button is not showing up in BillingPage.
Test the full end-to-end flow
Everything is wired up. Time to test the complete billing flow from start to finish.
- In your browser, navigate to the patients list and click any Patient ID link.
- Enter values in the charge fields. Try filling in all four: Consultation Fee, Room Charges, Vaccines, and Extra / Miscellaneous.
- Confirm the total updates as you type.
- Click the Print Invoice button.
The browser's print dialog opens. In the print preview, you should see a clean invoice with the Korutla Hospital header, the patient's details, an itemized list of only the charges you entered, the total, and signature lines at the bottom. No form fields, no navigation bar, and no buttons appear in the preview.
What about the document title?
Check the filename shown in the print dialog (or the "Save as PDF" filename if you choose that option). It should follow the pattern Invoice_KRL-YYNNNN_YYYY-MM-DD with the patient's unique ID and today's date. This comes from the documentTitle function you configured in useReactToPrint.
- Close the print dialog (click Cancel or press Escape).
- Try clearing all the charge amounts back to zero. The Print Invoice button should become greyed out and unclickable again.
Print preview shows the entire page?
- This means react-to-print is not isolating the invoice content. Check that contentRef: invoiceRef is set in the useReactToPrint options.
- Make sure the invoice div has ref={invoiceRef} on it. Without this, the hook does not know which element to print.
My print preview is showing the whole page instead of just the invoice. How do I fix this?
✔️ Awesome, I've got everything!
Great. Double check you've saved BillingPage.tsx before moving on.
ⓧ I'd like to double check the full code
Here is the complete BillingPage.tsx file. Compare your code against this:
import { useEffect, useState, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import { useReactToPrint } from "react-to-print";
import axiosClient from "../api/axiosClient";
interface Patient {
id: number;
patientUniqueId: string;
firstName: string;
lastName: string;
dateOfBirth: string;
gender: string;
phoneNumber: string;
address: string;
patientType: string;
registeredAt: string;
}
interface ChargeItem {
description: string;
amount: number;
}
export default function BillingPage() {
const { id } = useParams();
const navigate = useNavigate();
const invoiceRef = useRef<HTMLDivElement>(null);
const [patient, setPatient] = useState<Patient | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [consultation, setConsultation] = useState<ChargeItem>({
description: "General Consultation",
amount: 0,
});
const [roomCharges, setRoomCharges] = useState<ChargeItem>({
description: "Room Charges",
amount: 0,
});
const [vaccines, setVaccines] = useState<ChargeItem>({
description: "",
amount: 0,
});
const [extra, setExtra] = useState<ChargeItem>({
description: "",
amount: 0,
});
const total =
consultation.amount + roomCharges.amount + vaccines.amount + extra.amount;
const handlePrint = useReactToPrint({
contentRef: invoiceRef,
documentTitle: () =>
`Invoice_${patient?.patientUniqueId ?? "unknown"}_${new Date().toISOString().split("T")[0]}`,
pageStyle: `
@page { size: A4; margin: 20mm; }
@media print {
body { -webkit-print-color-adjust: exact; }
}
`,
});
useEffect(() => {
axiosClient
.get(`/api/patients/${id}`)
.then((res) => {
setPatient(res.data);
setLoading(false);
})
.catch(() => {
setError("Patient not found.");
setLoading(false);
});
}, [id]);
if (loading) return <div style={{ padding: 20 }}>Loading patient...</div>;
if (error)
return (
<div style={{ padding: 20 }}>
<p style={{ color: "red" }}>{error}</p>
<button
onClick={() => navigate("/patients")}
style={{ padding: "8px 16px" }}
>
Back to Patients
</button>
</div>
);
if (!patient) return null;
return (
<div style={{ padding: 20 }}>
<button
onClick={() => navigate("/patients")}
style={{ marginBottom: 16, padding: "8px 16px" }}
>
Back to Patients
</button>
<h2>
Billing - {patient.firstName} {patient.lastName} ({patient.patientUniqueId})
</h2>
<div
style={{
border: "1px solid #ccc",
padding: 16,
marginBottom: 24,
borderRadius: 4,
}}
>
<h3 style={{ marginTop: 0 }}>Enter Charges</h3>
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th style={formThStyle}>Charge Type</th>
<th style={formThStyle}>Description</th>
<th style={formThStyle}>Amount (INR)</th>
</tr>
</thead>
<tbody>
<tr>
<td style={formTdStyle}>Consultation Fee</td>
<td style={formTdStyle}>
<input
value={consultation.description}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={consultation.amount || ""}
onChange={(e) =>
setConsultation((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Room Charges</td>
<td style={formTdStyle}>
<input
value={roomCharges.description}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={roomCharges.amount || ""}
onChange={(e) =>
setRoomCharges((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Vaccines</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Hepatitis B, Tetanus"
value={vaccines.description}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={vaccines.amount || ""}
onChange={(e) =>
setVaccines((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
<tr>
<td style={formTdStyle}>Extra / Miscellaneous</td>
<td style={formTdStyle}>
<input
placeholder="e.g. Lab tests, X-ray"
value={extra.description}
onChange={(e) =>
setExtra((prev) => ({
...prev,
description: e.target.value,
}))
}
style={inputStyle}
/>
</td>
<td style={formTdStyle}>
<input
type="number"
min="0"
value={extra.amount || ""}
onChange={(e) =>
setExtra((prev) => ({
...prev,
amount: parseFloat(e.target.value) || 0,
}))
}
style={inputStyle}
/>
</td>
</tr>
</tbody>
</table>
<div
style={{
textAlign: "right",
marginTop: 12,
fontSize: 18,
fontWeight: "bold",
}}
>
Total: INR {total.toFixed(2)}
</div>
</div>
<button
onClick={handlePrint}
disabled={total === 0}
style={{
padding: "10px 24px",
marginBottom: 24,
fontSize: 16,
cursor: total === 0 ? "not-allowed" : "pointer",
}}
>
Print Invoice
</button>
<div
ref={invoiceRef}
style={{
border: "1px solid #ccc",
padding: 32,
maxWidth: 700,
margin: "0 auto",
fontFamily: "serif",
}}
>
<div style={{ textAlign: "center", marginBottom: 24 }}>
<h1 style={{ margin: 0 }}>Korutla Hospital</h1>
<p style={{ margin: "4px 0", color: "#555" }}>
Korutla, Jagtial District, Telangana
</p>
<p style={{ margin: "4px 0", color: "#555" }}>
Phone: +91-XXXXX-XXXXX
</p>
<hr />
</div>
<h2 style={{ textAlign: "center", marginBottom: 16 }}>
Patient Bill / Invoice
</h2>
<table style={{ width: "100%", marginBottom: 16 }}>
<tbody>
<tr>
<td>
<strong>Patient ID:</strong> {patient.patientUniqueId}
</td>
<td>
<strong>Date:</strong>{" "}
{new Date().toLocaleDateString("en-IN")}
</td>
</tr>
<tr>
<td>
<strong>Name:</strong> {patient.firstName} {patient.lastName}
</td>
<td>
<strong>Gender:</strong> {patient.gender}
</td>
</tr>
<tr>
<td>
<strong>Phone:</strong> {patient.phoneNumber}
</td>
<td>
<strong>Type:</strong> {patient.patientType}
</td>
</tr>
<tr>
<td colSpan={2}>
<strong>Address:</strong> {patient.address}
</td>
</tr>
</tbody>
</table>
<table
style={{
width: "100%",
borderCollapse: "collapse",
marginBottom: 16,
}}
>
<thead>
<tr>
<th style={invoiceThStyle}>S.No</th>
<th style={invoiceThStyle}>Description</th>
<th style={{ ...invoiceThStyle, textAlign: "right" }}>
Amount (INR)
</th>
</tr>
</thead>
<tbody>
{[consultation, roomCharges, vaccines, extra]
.filter((item) => item.amount > 0)
.map((item, index) => (
<tr key={index}>
<td style={invoiceTdStyle}>{index + 1}</td>
<td style={invoiceTdStyle}>
{item.description || "N/A"}
</td>
<td style={{ ...invoiceTdStyle, textAlign: "right" }}>
{item.amount.toFixed(2)}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td
colSpan={2}
style={{
...invoiceTdStyle,
fontWeight: "bold",
textAlign: "right",
borderTop: "2px solid #333",
}}
>
Total
</td>
<td
style={{
...invoiceTdStyle,
fontWeight: "bold",
textAlign: "right",
borderTop: "2px solid #333",
}}
>
{total.toFixed(2)}
</td>
</tr>
</tfoot>
</table>
<div
style={{
marginTop: 40,
display: "flex",
justifyContent: "space-between",
}}
>
<div>
<p>____________________</p>
<p>Authorized Signature</p>
</div>
<div>
<p>____________________</p>
<p>Patient / Attendant</p>
</div>
</div>
<p
style={{
textAlign: "center",
marginTop: 32,
fontSize: 12,
color: "#888",
}}
>
Thank you for choosing Korutla Hospital. Get well soon!
</p>
</div>
</div>
);
}
const formThStyle = {
borderBottom: "2px solid #ccc",
textAlign: "left" as const,
padding: 8,
};
const formTdStyle = {
borderBottom: "1px solid #eee",
padding: 8,
};
const inputStyle = {
width: "100%",
padding: 6,
boxSizing: "border-box" as const,
};
const invoiceThStyle = {
borderBottom: "2px solid #333",
textAlign: "left" as const,
padding: 8,
};
const invoiceTdStyle = {
borderBottom: "1px solid #ddd",
padding: 8,
};
Secret mission
Save Bills to the Backend and Show Billing History
Your billing page prints invoices, but the hospital has no record of past bills. What happens when a patient disputes a charge or needs a duplicate copy? In this secret mission, add backend persistence for bills and display billing history per patient.
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:
- Running .NET API server (HospitalApi on localhost:5057).
- Running React dev server (hospital-frontend).
- PostgreSQL Bills table (created via the AddBills EF Core migration).
- react-to-print npm package in hospital-frontend.
- New source files: Bill.cs, BillsController.cs, CreateBillDto.cs, BillResponseDto.cs, BillingPage.tsx.
- Modified source files: PatientsPage.tsx, App.tsx, HospitalDbContext.cs.
Keep everything running
No action needed. Choose this if you are still actively building on your hospital system or plan to continue adding features.
- Your .NET API and React dev server continue running in their terminal windows.
- The Bills table stays in your PostgreSQL database, ready for new billing data.
- All source files remain in your korutla-hospital project.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your source files and database stay intact so you can pick up where you left off.
- Switch to the terminal running the .NET API server and press Ctrl+C to stop it.
- Switch to the terminal running the React dev server and press Ctrl+C to stop it.
- Your PostgreSQL database, source files, and installed packages remain on disk. When you come back, restart both servers to continue.
Delete - I don't want to use this again
Remove all billing-related resources this project added. This reverts the billing feature from your hospital system while keeping the base project intact.
Stop both servers:
- Switch to the terminal running the .NET API server and press Ctrl+C to stop it.
- Switch to the terminal running the React dev server and press Ctrl+C to stop it.
Revert the database migration:
- In the terminal, navigate to the HospitalApi directory.
- List all migrations by running:
dotnet ef migrations list
- Find the migration name listed immediately before AddBills. Revert the database to that migration by running:
dotnet ef database update [[PREV_MIGRATION="name of the migration before AddBills"]]
- Remove the migration files by running:
dotnet ef migrations remove
Delete new backend files:
- From the HospitalApi directory, delete the new files by running:
rm Models/Bill.cs
rm Controllers/BillsController.cs
rm DTOs/CreateBillDto.cs
rm DTOs/BillResponseDto.cs
- Open HospitalDbContext.cs in HospitalApi/Data/ and remove the Bills DbSet line and the Bill entity configuration block you added.
Remove billing from the frontend:
- Navigate to the hospital-frontend directory.
- Uninstall the react-to-print package by running:
npm uninstall react-to-print
- Delete the billing page by running:
rm src/pages/BillingPage.tsx
- Open App.tsx in src/ and remove the BillingPage import line and the /patients/:id/billing route block.
- Open PatientsPage.tsx in src/pages/ and replace the Link wrapper around each Patient ID cell with plain text so the cell just displays patientUniqueId without a link.
Nice Work!
Nice Work!
Nice work! You just extended a full-stack hospital management system with a complete billing workflow, from clickable patient navigation all the way to a professional printed invoice.
You've learned how to:
- Updated the patients list with clickable Patient IDs using React Router params to navigate to a dedicated billing page for each patient.
- Built a billing form with four charge slots (Consultation Fee, Room Charges, Vaccines, Extra) and a live-updating total powered by React state.
- Created a professional printable invoice layout with hospital branding, patient details, itemized charges, and signature lines, then integrated react-to-print to trigger the browser's native print dialog with only the invoice content.
- Secret Mission: Added backend billing persistence with a Bill entity, EF Core migration, and API endpoints, plus a billing history display on the frontend.
Ready to quiz yourself?