Build an Emergency Routing Dashboard
Build a fictional dashboard that explains hospital transfer routing decisions.
Introduction
30 Second Summary
The closest hospital can feel like the obvious destination during an urgent transfer. Its distance says nothing about whether it has the required specialty or an available bed.
In this project, you will build a browser-based emergency routing control centre using synthetic data. The finished simulation exposes every ranking reason while a human operator controls approval, override, and rerouting.
What You'll Build
You select a fictional transfer request and watch the dashboard reject unsuitable hospitals before a human operator approves the best eligible route.
By the end of this project, you'll have:
- A transparent hospital ranking for a RED Trauma request that shows each eligibility decision plus its score calculation.
- Direct operator control over every recommendation through approval or a justified override recorded in the activity log.
- A capacity-crisis demo where a hospital loses all available beds. You can show the recalculated route through updated presentation metrics.
- Secret Mission: Run a regional capacity-crisis drill and defend a human override of the recommendation.
Are there any prerequisites?
You need a Windows computer with internet access so you can download Visual Studio Code. You do not need coding experience, Python, a cloud account, or patient data.
Before We Start
Before any hands-on work begins, commit to building a fictional hospital-routing prototype for education only. It stays outside diagnosis, clinical triage, and real emergency care because a human control-centre operator makes every final decision.
Set Up Your Windows Workspace
The safety boundary for your fictional routing prototype is set. Your Windows computer now needs a local home for the simulator.
A local HTML project lets a browser read the page directly. It needs no Python installation or package manager.
Visual Studio Code gives you an editor with a built-in preview. This path also avoids cloud accounts, API keys, external services, and ongoing costs.
In this step, get ready to:
- Install Visual Studio Code without signing in.
- Prepare the local er-control-centre folder as your workspace.
- Prepare index.html for a blank preview in the Integrated Browser.
Install Visual Studio Code
The Windows User setup is recommended for most people. It works without administrator permissions.
The installer may show a Windows confirmation before setup begins. Approving it starts the official installer you downloaded.
- Visit the official Visual Studio Code download page in your browser.
- Select the Windows User setup download.
- Open the downloaded installer from your browser's downloads list.
- Approve the Windows confirmation if it appears.
- Complete the installer using its default options.
The installation adds Visual Studio Code as a local Windows app. An account is optional for its core editor features.
- Press the Windows logo key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open the editor.
- Leave any sign-in option unused.
That clears the only software installation in this project. Visual Studio Code is open without an account or credentials.
Visual Studio Code not opening?
- Confirm that you downloaded the Windows User setup from the official page.
- Close any incomplete installer window before reopening the downloaded installer.
- Restart Windows if Visual Studio Code does not appear in search after installation.
Help me troubleshoot this Visual Studio Code installation.
Create the local project folder
File Explorer gives the simulator a folder you can find again. Visual Studio Code treats the folder you open as the boundary for your project files.
- Press the Windows logo key + E to open File Explorer.
- Navigate to your desktop in File Explorer.
- Select New in the toolbar.
- Select Folder.
- Type er-control-centre as the folder name.
- Press Enter to create the folder.
Your desktop now contains the er-control-centre folder. This folder is the local home for every project file.
Visual Studio Code may show a folder safety prompt when you open your workspace. You can trust this folder because you created it for this project.
- Return to Visual Studio Code from the Windows taskbar.
- Click File in the top menu.
- Click Open Folder....
- Select the er-control-centre folder on your desktop.
- Click Select Folder.
- Confirm that you trust the folder if the safety prompt appears.
- Confirm that er-control-centre appears in the Explorer sidebar.
Your workspace is now open in the editor. Every file you create through the Explorer stays inside er-control-centre.
Folder missing from the Explorer?
- Confirm that you selected the er-control-centre folder itself in the folder picker.
- Repeat File followed by Open Folder... if another location appears in the Explorer.
Help me open the correct project folder in Visual Studio Code.
Create and preview index.html
An HTML document stores page settings inside <head>. Visible page content belongs inside <body>.
Why a single HTML file?
A single local HTML file keeps the first preview close to the code you edit. The browser can load it without a separate runtime.
A Python server would add installation work before you could see the dashboard. This approach gets you to a visual prototype sooner.
- Select the er-control-centre folder in the Explorer sidebar.
- Click the New File... button in the Explorer toolbar.
- Enter index.html as the file name.
- Press Enter to create the file.
- Add the minimal page structure to index.html by pasting this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emergency Routing Control Centre</title>
</head>
<body>
</body>
</html>
What does this HTML do?
- The <!doctype html> declaration tells the browser to use modern HTML.
- The <html lang="en"> line identifies English as the document language.
- The <meta charset="UTF-8"> line configures the document's text encoding.
- The viewport metadata prepares the future dashboard for different screen widths.
- The <title> line names the page Emergency Routing Control Centre.
- The empty <body> creates the blank starting page.
- Save index.html.
Before you preview it, make a quick prediction: visible content or a blank page.
- Right-click index.html in the Explorer sidebar.
- Select Open in Integrated Browser.
The Integrated Browser opens a blank local page in a new editor tab. You have proved that your saved HTML file loads inside Visual Studio Code.
Can't open the local preview?
- Confirm that index.html is saved before opening its context menu again.
- Press F1 to open the Command Palette if the browser action is unavailable.
- Enter Browser: Open Integrated Browser into the Command Palette.
- Select the matching command to open the built-in browser.
Help me open my saved HTML file in the Integrated Browser.
✔️ Awesome, I've got everything!
Great. Double-check that you saved index.html inside er-control-centre.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emergency Routing Control Centre</title>
</head>
<body>
</body>
</html>
Your local page is loading from the finished workspace. Next, you will turn that blank page into the visible control-centre dashboard.
Build the Dashboard
Your local preview is ready for the control-centre workflow. The dashboard needs to place the transfer request beside the hospital evidence that supports each routing decision.
You will build the visible interface with HTML and CSS. You will then use JavaScript to test a first decision rule based on travel time.
In this step, get ready to:
- Build the control-centre interface and safety warning.
- Render three fictional hospitals from synthetic data.
- Test the first routing recommendation against the visible hospital network.
Assemble the control-centre structure
The dashboard shell keeps the fictional transfer request and hospital network visible on one screen. This gives the operator evidence to inspect before recording any decision.
- In index.html, replace the empty <body> section with this page frame:
<body>
<header class="topbar">
<div>
<p class="eyebrow">Regional Operations Simulation</p>
<h1>Emergency Routing Control Centre</h1>
<p>Explainable destination support for a human control-centre operator</p>
</div>
<div class="network-status"><span class="pulse"></span>Simulation network active</div>
</header>
<main>
<section class="notice">
<strong>Educational simulation only.</strong> All patients, hospitals, capacities, travel times, and scores are fictional. This page must not be used for diagnosis, clinical triage, dispatch, or real patient care.
</section>
<div class="grid">
</div>
</main>
</body>
What does this structure do?
- The header identifies the page as a regional operations simulation.
- The warning establishes the educational safety boundary before any controls appear.
- The grid container gives every dashboard panel one shared layout area.
- Save index.html.
- Switch back to the Integrated Browser tab from earlier.
- Refresh the local preview.
You will see the dashboard title and the fictional-data warning. The blank page now has a clear operational boundary.
Still seeing a blank page?
Confirm that the new markup sits between <body> and </body>. Check that index.html is saved.
Refresh the existing Integrated Browser tab after saving. Help me find why my local HTML preview is still blank.
The first panel captures a clinician-assigned acuity category. It also captures the specialty that the receiving hospital must provide.
- In index.html, locate the empty <div class="grid"> element.
- Paste this transfer-request panel inside the empty grid:
<section class="card">
<h2>Synthetic transfer request</h2>
<p class="muted">The acuity category is assumed to have been assigned by trained clinical staff.</p>
<label for="acuity">Clinician-assigned acuity</label>
<select id="acuity">
<option value="red">RED: High acuity; need to be seen immediately</option>
<option value="yellow">YELLOW: Moderate acuity; need to be seen soon</option>
<option value="green">GREEN: Low acuity; can wait</option>
</select>
<label for="specialty">Required specialty</label>
<select id="specialty">
<option value="Trauma">Trauma</option>
<option value="Cardiac">Cardiac</option>
<option value="General">General</option>
</select>
<div class="button-row">
<button id="analyzeButton" type="button">Analyze request</button>
<button id="resetButton" class="secondary" type="button">Reset simulation</button>
</div>
</section>
What does this panel capture?
- The acuity selector records a category already assigned by trained clinical staff.
- The specialty selector records the capability required from the destination.
- The analyzeButton starts the routing calculation once JavaScript is connected.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see selectors for acuity and specialty. You will also see controls for analysis and reset.
Transfer controls missing?
Check that the new <section class="card"> sits inside <div class="grid">. A section placed after the grid's closing tag will appear outside the dashboard layout.
Confirm that every <select> element has a closing tag. Help me check the transfer-request HTML structure.
The next panels make hospital evidence and the recommendation visible at the same time. This keeps the result open to human review.
- Place these hospital and recommendation panels below the transfer-request section inside the grid:
<section class="card">
<h2>Fictional hospital network</h2>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Hospital</th>
<th>Travel</th>
<th>Beds</th>
<th>Specialties</th>
</tr>
</thead>
<tbody id="hospitalRows"></tbody>
</table>
</div>
</section>
<section class="card wide">
<span id="acuityBadge" class="badge">Awaiting request</span>
<h2 id="recommendationTitle">No destination recommendation yet</h2>
<p id="recommendationReason" class="muted">Select the synthetic request details and analyze the network.</p>
<p class="formula">Project formula: 100 - (travel time × acuity weight) + (available beds × 4). Higher scores rank first after mandatory eligibility checks.</p>
<div id="rankingList" class="ranking-list"></div>
</section>
How do these panels support review?
- The hospitalRows element receives the fictional hospital records.
- The recommendation elements separate the selected destination from its explanation.
- The rankingList area provides space for visible scoring or rejection details.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see the hospital table headings and an empty recommendation panel. The network rows arrive when you add the fictional records.
Recommendation panel out of place?
Confirm that both new sections appear before the grid's closing </div> tag. Check that the table includes <tbody id="hospitalRows">.
A missing closing section tag can pull later panels into the table. Help me find the misplaced dashboard tag.
A recommendation needs an explicit human decision point. The controls below reserve space for approval and override actions that you will activate later.
- Place this decision panel below the recommendation panel inside the grid:
<section class="card">
<h2>Human control-centre decision</h2>
<label for="operatorChoice">Eligible destination</label>
<select id="operatorChoice" disabled>
<option>Analyze a request first</option>
</select>
<label for="operatorNote">Operator note</label>
<textarea id="operatorNote" placeholder="Optional operational reason for approval or override"></textarea>
<div class="button-row">
<button id="approveButton" type="button" disabled>Approve recommendation</button>
<button id="overrideButton" class="warning" type="button" disabled>Record override</button>
</div>
<p id="decisionFeedback" class="feedback" aria-live="polite"></p>
</section>
Why is the decision panel disabled?
The controls start disabled because the operator has no analyzed destination to review. This prevents a decision from being recorded without a recommendation.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see a destination selector and operator-note field. The decision buttons remain disabled until a later step connects the human approval workflow.
Decision fields not disabled?
Check that disabled appears on operatorChoice and both decision buttons. The note field remains available because it does not submit a decision by itself.
Confirm that each control uses the identifier shown in the snippet. Help me verify the operator-control identifiers.
Metrics and the activity log turn individual actions into an operational story. The briefing keeps the method and safety boundary visible during a demonstration.
- Place this metrics panel below the decision panel inside the grid:
<section class="card">
<h2>Control-centre metrics</h2>
<div class="metric-grid">
<div class="metric"><span>Decisions</span><strong id="decisionCount">0</strong></div>
<div class="metric"><span>Overrides</span><strong id="overrideCount">0</strong></div>
<div class="metric"><span>Capacity events</span><strong id="eventCount">0</strong></div>
</div>
<button id="capacityButton" class="warning" type="button" disabled>Simulate capacity loss</button>
</section>
What do these metrics track?
- Decisions counts every recorded operator action.
- Overrides counts decisions that differ from the recommendation.
- Capacity events counts simulated changes to the hospital network.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see three counters set to zero. The capacity control remains disabled until the dashboard has a current recommendation.
Metrics missing or misaligned?
Confirm that all three metric elements sit inside metric-grid. Check that each counter starts with the value 0.
A missing closing </div> tag can merge this panel with the decision panel. Help me check the metrics markup.
- Place these activity-log and briefing panels below the metrics panel inside the grid:
<section class="card wide">
<h2>Activity log</h2>
<ol id="activityLog">
<li>Simulation ready. No patient data has been loaded.</li>
</ol>
</section>
<section class="card wide">
<h2>Presentation briefing</h2>
<div class="briefing">
<ul id="briefingList">
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Apply mandatory eligibility checks, then rank suitable fictional hospitals.</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
</ul>
</div>
</section>
Why keep these panels visible?
The activity log provides an auditable sequence of simulation events. The briefing gives the operator a concise explanation of the problem and safety boundary.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see the initial activity entry and four briefing points. The complete dashboard structure is now visible.
Briefing list missing?
Check that briefingList appears on the inner <ul> element. Confirm that both sections remain inside the grid.
Make sure the grid closes after the briefing section. Help me check the final dashboard structure.
Style the dashboard
CSS turns the stacked controls into a control-centre layout. The visual hierarchy keeps the safety warning and recommendation easy to find.
- In index.html, locate the <title> line inside <head>.
- Paste this base style block below the title:
<style>
:root {
--navy: #10233f;
--blue: #1769aa;
--light-blue: #eaf4ff;
--green: #16794b;
--yellow: #b26a00;
--red: #b42318;
--ink: #172033;
--muted: #5d687a;
--line: #d9e0ea;
--surface: #ffffff;
--background: #f3f6fa;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
color: var(--ink);
background: var(--background);
}
</style>
What do these base styles do?
- The custom properties hold the dashboard's reusable colors.
- The universal box model keeps padding inside each element's measured width.
- The body rules establish the page font and background.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see a pale background and dark page text. The dashboard now has a consistent visual foundation.
Base colors not showing?
Confirm that the complete <style> block sits inside <head>. Check that each custom property starts with two hyphens.
Refresh the preview after saving the file. Help me diagnose why my CSS variables are not applying.
- Add these header styles immediately above the closing </style> tag:
.topbar {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: center;
padding: 24px clamp(20px, 5vw, 64px);
color: white;
background: linear-gradient(120deg, var(--navy), #174b76);
}
.eyebrow {
margin: 0 0 6px;
color: #a8d7ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 8px;
font-size: clamp(1.7rem, 4vw, 2.6rem);
}
How does the header styling help?
The flex layout separates the page identity from the network status. The color contrast makes the simulation header distinct from the working panels.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see a navy gradient header with white text. The simulation label appears above the main title.
Header still looks unstyled?
Check that the selector is written as .topbar. Confirm that the header uses class="topbar".
Make sure these rules appear before the closing style tag. Help me find why the topbar styles are not matching.
- Add these layout styles immediately above the closing </style> tag:
main {
width: min(1180px, calc(100% - 32px));
margin: 24px auto 48px;
}
.notice {
margin-bottom: 18px;
padding: 14px 16px;
border-left: 5px solid var(--yellow);
border-radius: 8px;
background: #fff7e8;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.card {
padding: 20px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 8px 24px rgba(16, 35, 63, 0.06);
}
What changes in the layout?
- The main content gains a readable maximum width.
- The warning receives a colored safety accent.
- The grid places dashboard cards into two columns.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see white cards arranged in two columns. The safety warning appears in a pale yellow panel above them.
Cards still stacked without spacing?
Confirm that the dashboard sections use class="card". Check that their parent uses class="grid".
A narrow Integrated Browser panel can reduce the available width. Help me check why the grid is not forming two columns.
- Add these control styles immediately above the closing </style> tag:
.button-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
button {
padding: 10px 14px;
border: 0;
border-radius: 8px;
color: white;
background: var(--blue);
cursor: pointer;
font-weight: 700;
}
button.secondary {
color: var(--navy);
background: #dcecff;
}
button.warning {
background: var(--yellow);
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
What do the control styles communicate?
Primary controls use blue while simulation controls use yellow. Disabled controls become visibly muted until the workflow makes them available.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see rounded blue and yellow buttons. The unavailable decision controls appear faded.
Buttons still use browser defaults?
Check that the button selector has no leading dot. Confirm that the secondary and warning controls retain their class names.
Look for a missing closing brace in the rule above this chunk. Help me locate the CSS syntax problem.
- Add these table styles immediately above the closing </style> tag:
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
padding: 10px 8px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
}
Why style the network as a table?
Aligned columns make travel time and available beds easy to compare. Compact headings keep the fictional network readable without dominating the page.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see aligned hospital columns with uppercase headings. The table is ready to receive the fictional records.
Table headings not aligned?
Confirm that the table retains its <thead> and <tbody> elements. Check that the th and td rules appear inside the style block.
Look for an extra closing style tag above these rules. Help me troubleshoot the table styling.
- Add these recommendation styles immediately above the closing </style> tag:
.badge {
display: inline-block;
margin-bottom: 10px;
padding: 6px 10px;
border-radius: 999px;
color: white;
background: var(--muted);
font-size: 0.8rem;
font-weight: 800;
}
.badge.red {
background: var(--red);
}
.formula {
padding: 12px;
border-radius: 8px;
color: #0d426d;
background: var(--light-blue);
font-family: Consolas, monospace;
}
How does the recommendation stand out?
The badge makes the selected acuity visible beside the result. The formula panel separates the teaching method from the recommendation text.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see a pill-shaped status badge and a pale blue formula panel. The recommendation area now has a clear visual hierarchy.
Acuity badge not changing shape?
Confirm that acuityBadge has class="badge" in the HTML. Check that both badge rules are inside the style block.
The red color appears after the analysis code adds the red class. Help me verify the badge selectors.
- Add these ranking and metric styles immediately above the closing </style> tag:
.ranking-list {
display: grid;
gap: 10px;
margin-top: 14px;
}
.ranking-item {
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.metric {
padding: 14px;
border-radius: 10px;
background: var(--light-blue);
text-align: center;
}
What do these final styles add?
The ranking area gains a bordered evidence card. The three metrics become equally sized tiles that can update during the simulation.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see three blue metric tiles and a reserved ranking area. The control-centre shell is now presentation ready.
Metric tiles not forming a row?
Check that all three metric elements remain inside metric-grid. Confirm that the grid rule contains three columns.
Widen the Integrated Browser panel if the editor leaves little preview space. Help me troubleshoot the metric layout.
Add fictional data and test the first rule
Synthetic data lets you test routing behavior without patient records or real hospital information. Each fictional hospital has a different combination of travel time, beds, and specialties.
- In index.html, locate the closing </main> tag.
- Paste this script block between </main> and </body>:
<script>
const hospitals = [
{
id: "northside",
name: "Northside Community Hospital",
travelTime: 6,
availableBeds: 5,
initialBeds: 5,
specialties: ["General", "Cardiac"]
},
{
id: "central",
name: "Central Trauma Centre",
travelTime: 14,
availableBeds: 2,
initialBeds: 2,
specialties: ["General", "Trauma", "Cardiac"]
},
{
id: "lakeside",
name: "Lakeside Regional Hospital",
travelTime: 22,
availableBeds: 6,
initialBeds: 6,
specialties: ["General", "Trauma"]
}
];
</script>
What does the hospital data represent?
- Northside has the shortest travel time but does not list Trauma.
- Central provides every listed specialty with two beds available.
- Lakeside provides Trauma and General care with six beds available.
- Save index.html.
- Refresh the Integrated Browser preview.
The page still loads with the same dashboard shell. The browser has now loaded three fictional hospital records for the network table.
Page disappeared after adding the script?
Confirm that the <script> block appears after </main>. Check every hospital object for matching braces and commas.
Make sure the script closes before </body>. Help me find the JavaScript syntax error.
Acuity details convert each selector value into a visible label and travel weight. The remaining state values prepare the dashboard for recommendations and later operator actions.
- Place these state values below the hospitals array inside the script:
const acuityDetails = {
red: {
label: "RED",
description: "High acuity; need to be seen immediately",
travelWeight: 3
},
yellow: {
label: "YELLOW",
description: "Moderate acuity; need to be seen soon",
travelWeight: 2
},
green: {
label: "GREEN",
description: "Low acuity; can wait",
travelWeight: 1
}
};
const metrics = {
decisions: 0,
overrides: 0,
events: 0
};
let currentRecommendation = null;
let currentEligibleHospitals = [];
What state does the dashboard keep?
- The acuityDetails object maps selector values to labels and teaching weights.
- The metrics object starts every counter at zero.
- The current recommendation variables hold the latest routing result.
- Save index.html.
- Refresh the Integrated Browser preview.
The dashboard continues to load with all counters at zero. Its in-memory state is ready for the first analysis.
Seeing a script error?
Check the comma between each acuity entry. Confirm that metrics ends with a closing brace and semicolon.
Make sure both current-state variables remain inside the script. Help me inspect the dashboard state declarations.
The hospital table starts empty because the records live in JavaScript. A render function turns each record into one visible table row.
- Place this rendering function below the state values inside the script:
function renderHospitals() {
const rows = hospitals.map((hospital) => `
<tr>
<td><strong>${hospital.name}</strong></td>
<td>${hospital.travelTime} min</td>
<td>${hospital.availableBeds}</td>
<td>${hospital.specialties.join(", ")}</td>
</tr>
`).join("");
document.getElementById("hospitalRows").innerHTML = rows;
}
What does this function do?
- The map() operation creates one table row for each hospital.
- The specialty list is joined into readable comma-separated text.
- The completed rows are inserted into hospitalRows.
- Place this function call below renderHospitals() inside the script:
renderHospitals();
Why call the function here?
The function call renders the hospital records as soon as the page loads. The operator can inspect the network before analyzing a request.
- Save index.html.
- Refresh the Integrated Browser preview.
You will see Northside Community Hospital, Central Trauma Centre, and Lakeside Regional Hospital in the network table. Each row shows travel time, beds, and specialties.
Hospital rows not appearing?
Confirm that renderHospitals(); appears after the function definition. Check that the table body uses the exact identifier hospitalRows.
Look for a missing backtick around the table-row template. Help me debug the empty hospital table.
The first routing rule compares travel time only. Running it against a Trauma request lets you judge the recommendation against the visible specialty evidence.
- Remove the standalone renderHospitals(); call from the bottom of the script.
- Place this first recommendation flow below renderHospitals() inside the script:
function recommendHospital() {
return [...hospitals].sort(
(first, second) => first.travelTime - second.travelTime
)[0];
}
function analyzeRequest() {
const acuity = document.getElementById("acuity").value;
const requiredSpecialty = document.getElementById("specialty").value;
currentRecommendation = recommendHospital();
currentEligibleHospitals = [...hospitals];
const badge = document.getElementById("acuityBadge");
badge.textContent = `${acuityDetails[acuity].label}: ${acuityDetails[acuity].description}`;
badge.className = `badge ${acuity}`;
document.getElementById("recommendationTitle").textContent = `Recommended destination: ${currentRecommendation.name}`;
document.getElementById("recommendationReason").textContent = `${currentRecommendation.name} has the shortest travel time at ${currentRecommendation.travelTime} minutes.`;
document.getElementById("rankingList").innerHTML = `
<div class="ranking-item">
<strong>Travel-time-only rule</strong>
<span>Required: ${requiredSpecialty}. Available specialties: ${currentRecommendation.specialties.join(", ")}.</span>
</div>
`;
}
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
renderHospitals();
How does the first rule work?
- The recommendHospital() function sorts hospitals from shortest travel time to longest.
- The first hospital in that sorted list becomes currentRecommendation.
- The analysis panel shows the selected hospital beside its available specialties.
- Save index.html.
- Refresh the Integrated Browser preview.
Analyze button not responding?
Confirm that the event listener appears after analyzeRequest(). Check that the button identifier is exactly analyzeButton.
Look for a missing backtick in the recommendation templates. Help me debug the first recommendation flow.
Before you test the decision, which hospital do you expect a travel-time-only rule to choose?
- Select RED in the clinician-assigned acuity field.
- Select Trauma in the required specialty field.
- Click Analyze request.
You will see Northside Community Hospital recommended because its travel time is 6 minutes. The network table shows that Northside provides General and Cardiac care without Trauma.
This shortfall is intentional
The first rule selects the nearest destination without checking its capability. A Trauma request has reached a hospital that does not list Trauma.
You have exposed the weakness with visible evidence. The next routing rule must check mandatory eligibility before ranking destinations.
✔️ Awesome, I've got everything!
Great work. Your dashboard now renders fictional hospital data and exposes the travel-time-only routing problem.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emergency Routing Control Centre</title>
<style>
:root {
--navy: #10233f;
--blue: #1769aa;
--light-blue: #eaf4ff;
--green: #16794b;
--yellow: #b26a00;
--red: #b42318;
--ink: #172033;
--muted: #5d687a;
--line: #d9e0ea;
--surface: #ffffff;
--background: #f3f6fa;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
color: var(--ink);
background: var(--background);
}
.topbar {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: center;
padding: 24px clamp(20px, 5vw, 64px);
color: white;
background: linear-gradient(120deg, var(--navy), #174b76);
}
.eyebrow {
margin: 0 0 6px;
color: #a8d7ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 8px;
font-size: clamp(1.7rem, 4vw, 2.6rem);
}
main {
width: min(1180px, calc(100% - 32px));
margin: 24px auto 48px;
}
.notice {
margin-bottom: 18px;
padding: 14px 16px;
border-left: 5px solid var(--yellow);
border-radius: 8px;
background: #fff7e8;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.card {
padding: 20px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 8px 24px rgba(16, 35, 63, 0.06);
}
.button-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
button {
padding: 10px 14px;
border: 0;
border-radius: 8px;
color: white;
background: var(--blue);
cursor: pointer;
font-weight: 700;
}
button.secondary {
color: var(--navy);
background: #dcecff;
}
button.warning {
background: var(--yellow);
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
padding: 10px 8px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
}
.badge {
display: inline-block;
margin-bottom: 10px;
padding: 6px 10px;
border-radius: 999px;
color: white;
background: var(--muted);
font-size: 0.8rem;
font-weight: 800;
}
.badge.red {
background: var(--red);
}
.formula {
padding: 12px;
border-radius: 8px;
color: #0d426d;
background: var(--light-blue);
font-family: Consolas, monospace;
}
.ranking-list {
display: grid;
gap: 10px;
margin-top: 14px;
}
.ranking-item {
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.metric {
padding: 14px;
border-radius: 10px;
background: var(--light-blue);
text-align: center;
}
</style>
</head>
<body>
<header class="topbar">
<div>
<p class="eyebrow">Regional Operations Simulation</p>
<h1>Emergency Routing Control Centre</h1>
<p>Explainable destination support for a human control-centre operator</p>
</div>
<div class="network-status"><span class="pulse"></span>Simulation network active</div>
</header>
<main>
<section class="notice">
<strong>Educational simulation only.</strong> All patients, hospitals, capacities, travel times, and scores are fictional. This page must not be used for diagnosis, clinical triage, dispatch, or real patient care.
</section>
<div class="grid">
<section class="card">
<h2>Synthetic transfer request</h2>
<p class="muted">The acuity category is assumed to have been assigned by trained clinical staff.</p>
<label for="acuity">Clinician-assigned acuity</label>
<select id="acuity">
<option value="red">RED: High acuity; need to be seen immediately</option>
<option value="yellow">YELLOW: Moderate acuity; need to be seen soon</option>
<option value="green">GREEN: Low acuity; can wait</option>
</select>
<label for="specialty">Required specialty</label>
<select id="specialty">
<option value="Trauma">Trauma</option>
<option value="Cardiac">Cardiac</option>
<option value="General">General</option>
</select>
<div class="button-row">
<button id="analyzeButton" type="button">Analyze request</button>
<button id="resetButton" class="secondary" type="button">Reset simulation</button>
</div>
</section>
<section class="card">
<h2>Fictional hospital network</h2>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Hospital</th>
<th>Travel</th>
<th>Beds</th>
<th>Specialties</th>
</tr>
</thead>
<tbody id="hospitalRows"></tbody>
</table>
</div>
</section>
<section class="card wide">
<span id="acuityBadge" class="badge">Awaiting request</span>
<h2 id="recommendationTitle">No destination recommendation yet</h2>
<p id="recommendationReason" class="muted">Select the synthetic request details and analyze the network.</p>
<p class="formula">Project formula: 100 - (travel time × acuity weight) + (available beds × 4). Higher scores rank first after mandatory eligibility checks.</p>
<div id="rankingList" class="ranking-list"></div>
</section>
<section class="card">
<h2>Human control-centre decision</h2>
<label for="operatorChoice">Eligible destination</label>
<select id="operatorChoice" disabled>
<option>Analyze a request first</option>
</select>
<label for="operatorNote">Operator note</label>
<textarea id="operatorNote" placeholder="Optional operational reason for approval or override"></textarea>
<div class="button-row">
<button id="approveButton" type="button" disabled>Approve recommendation</button>
<button id="overrideButton" class="warning" type="button" disabled>Record override</button>
</div>
<p id="decisionFeedback" class="feedback" aria-live="polite"></p>
</section>
<section class="card">
<h2>Control-centre metrics</h2>
<div class="metric-grid">
<div class="metric"><span>Decisions</span><strong id="decisionCount">0</strong></div>
<div class="metric"><span>Overrides</span><strong id="overrideCount">0</strong></div>
<div class="metric"><span>Capacity events</span><strong id="eventCount">0</strong></div>
</div>
<button id="capacityButton" class="warning" type="button" disabled>Simulate capacity loss</button>
</section>
<section class="card wide">
<h2>Activity log</h2>
<ol id="activityLog">
<li>Simulation ready. No patient data has been loaded.</li>
</ol>
</section>
<section class="card wide">
<h2>Presentation briefing</h2>
<div class="briefing">
<ul id="briefingList">
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Apply mandatory eligibility checks, then rank suitable fictional hospitals.</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
</ul>
</div>
</section>
</div>
</main>
<script>
const hospitals = [
{
id: "northside",
name: "Northside Community Hospital",
travelTime: 6,
availableBeds: 5,
initialBeds: 5,
specialties: ["General", "Cardiac"]
},
{
id: "central",
name: "Central Trauma Centre",
travelTime: 14,
availableBeds: 2,
initialBeds: 2,
specialties: ["General", "Trauma", "Cardiac"]
},
{
id: "lakeside",
name: "Lakeside Regional Hospital",
travelTime: 22,
availableBeds: 6,
initialBeds: 6,
specialties: ["General", "Trauma"]
}
];
const acuityDetails = {
red: {
label: "RED",
description: "High acuity; need to be seen immediately",
travelWeight: 3
},
yellow: {
label: "YELLOW",
description: "Moderate acuity; need to be seen soon",
travelWeight: 2
},
green: {
label: "GREEN",
description: "Low acuity; can wait",
travelWeight: 1
}
};
const metrics = {
decisions: 0,
overrides: 0,
events: 0
};
let currentRecommendation = null;
let currentEligibleHospitals = [];
function renderHospitals() {
const rows = hospitals.map((hospital) => `
<tr>
<td><strong>${hospital.name}</strong></td>
<td>${hospital.travelTime} min</td>
<td>${hospital.availableBeds}</td>
<td>${hospital.specialties.join(", ")}</td>
</tr>
`).join("");
document.getElementById("hospitalRows").innerHTML = rows;
}
function recommendHospital() {
return [...hospitals].sort(
(first, second) => first.travelTime - second.travelTime
)[0];
}
function analyzeRequest() {
const acuity = document.getElementById("acuity").value;
const requiredSpecialty = document.getElementById("specialty").value;
currentRecommendation = recommendHospital();
currentEligibleHospitals = [...hospitals];
const badge = document.getElementById("acuityBadge");
badge.textContent = `${acuityDetails[acuity].label}: ${acuityDetails[acuity].description}`;
badge.className = `badge ${acuity}`;
document.getElementById("recommendationTitle").textContent = `Recommended destination: ${currentRecommendation.name}`;
document.getElementById("recommendationReason").textContent = `${currentRecommendation.name} has the shortest travel time at ${currentRecommendation.travelTime} minutes.`;
document.getElementById("rankingList").innerHTML = `
<div class="ranking-item">
<strong>Travel-time-only rule</strong>
<span>Required: ${requiredSpecialty}. Available specialties: ${currentRecommendation.specialties.join(", ")}.</span>
</div>
`;
}
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
renderHospitals();
</script>
</body>
</html>
How to use this reference
Compare this file with your saved index.html. Keep the nearest-only recommendation in place because the next step replaces that deliberate shortfall.
Your control-centre dashboard now makes the routing mistake visible beside the evidence that disproves it. Next, you will add mandatory capability and capacity checks before ranking eligible hospitals.
Add Explainable Eligibility Rules
The first dashboard run exposed a dangerous gap in nearest-only routing. Northside Community Hospital won on travel time despite lacking the required Trauma capability.
Now you will give the JavaScript decision engine an eligibility gate. A weighted routing score will compare only the hospitals that pass every mandatory check.
In this step, get ready to:
- Reject hospitals that lack capacity or the requested specialty.
- Rank each eligible hospital with a visible project formula.
- Show a complete explanation for every routing result.
Add mandatory eligibility checks
An eligibility gate applies the requirements that every destination must satisfy. Hospitals with no available beds or no matching specialty receive clear rejection reasons.
- Switch back to the open index.html tab in Visual Studio Code.
- Press Ctrl+F to open the editor search field.
- Enter function recommendHospital in the search field.
- Place your cursor directly above the temporary recommendHospital() function.
- Add the eligibility gate by pasting this function:
function getEligibleHospitals(requiredSpecialty) {
return hospitals.map((hospital) => {
const rejectionReasons = [];
if (hospital.availableBeds <= 0) {
rejectionReasons.push("No available beds");
}
if (!hospital.specialties.includes(requiredSpecialty)) {
rejectionReasons.push(`Does not provide ${requiredSpecialty}`);
}
return {
...hospital,
eligible: rejectionReasons.length === 0,
rejectionReasons
};
});
}
What does this eligibility gate do?
- The function evaluates every hospital in the fictional network.
- The availableBeds <= 0 check records No available beds when capacity reaches zero.
- The specialty check records Does not provide ${requiredSpecialty} when the requested capability is missing.
- The eligible flag becomes true when the rejection list is empty.
- Place your cursor directly below the closing brace of getEligibleHospitals().
- Add the project scoring formula by pasting this function:
function calculateRoutingScore(hospital, acuity) {
const travelWeight = acuityDetails[acuity].travelWeight;
return 100 - (hospital.travelTime * travelWeight) + (hospital.availableBeds * 4);
}
How does the project score work?
- The travelWeight value gives travel time a larger effect for more urgent acuity categories.
- Each available bed contributes four points to represent capacity.
- A larger score ranks higher after the mandatory eligibility checks.
- The formula uses fictional weights created for this educational simulation.
- Save index.html by pressing Ctrl+S.
- Switch back to the Integrated Browser tab from earlier.
- Refresh the local preview using its refresh control.
You should still see the safety warning plus all three fictional hospital rows. This confirms that both helper functions load without interrupting the dashboard.
Did the dashboard turn blank?
Check that both functions sit inside the existing <script> element. Compare every opening brace with its closing brace.
A missing quote can also stop the script from loading. Help me find the syntax problem in these eligibility and scoring functions.
Rank and render eligible hospitals
Filtering determines which hospitals may enter the ranking. The updated recommendation engine then scores those eligible destinations from highest to lowest.
- Return to the temporary recommendHospital() function you located earlier.
- Select the entire temporary function from its opening line through its closing brace.
- Replace the selected function by pasting this version:
function recommendHospital(acuity, requiredSpecialty) {
const evaluatedHospitals = getEligibleHospitals(requiredSpecialty);
const eligibleHospitals = evaluatedHospitals
.filter((hospital) => hospital.eligible)
.map((hospital) => ({
...hospital,
score: calculateRoutingScore(hospital, acuity)
}))
.sort((first, second) => second.score - first.score);
return {
evaluatedHospitals,
eligibleHospitals,
recommendation: eligibleHospitals[0] || null
};
}
What does the recommendation engine do?
- The evaluatedHospitals collection keeps every hospital plus its eligibility result.
- The filter() step removes rejected hospitals from score-based comparison.
- The map() step adds a score to each eligible hospital.
- The sort() step places the highest score first.
The recommendation panel needs the full evaluation result. Its renderer turns each decision into a visible destination, score calculation, or rejection reason.
- Place your cursor directly below the new recommendHospital() function.
- Keep index.html unsaved until both renderer parts are in place.
- Add the first part of the renderer by pasting this code:
function renderRecommendation(result, acuity, requiredSpecialty) {
const badge = document.getElementById("acuityBadge");
const title = document.getElementById("recommendationTitle");
const reason = document.getElementById("recommendationReason");
const rankingList = document.getElementById("rankingList");
const rankLookup = new Map(
result.eligibleHospitals.map((hospital, index) => [hospital.id, { ...hospital, rank: index + 1 }])
);
badge.textContent = `${acuityDetails[acuity].label}: ${acuityDetails[acuity].description}`;
badge.className = `badge ${acuity}`;
currentRecommendation = result.recommendation;
currentEligibleHospitals = result.eligibleHospitals;
if (result.recommendation) {
title.textContent = `Recommended destination: ${result.recommendation.name}`;
reason.textContent = `${result.recommendation.name} passed the bed and ${requiredSpecialty} checks, then ranked first with a score of ${result.recommendation.score}.`;
} else {
title.textContent = "No eligible destination in the fictional network";
reason.textContent = `No hospital currently has both an available bed and the required ${requiredSpecialty} capability. Escalation to a human coordinator is required.`;
}
What does the first renderer part prepare?
- The document references connect the JavaScript result to the existing recommendation panel.
- The rankLookup map assigns a visible position to each eligible hospital.
- The current recommendation state supports the operator controls added to the dashboard shell.
- The conditional message handles both a successful recommendation and an empty eligible list.
- Complete the same renderer by pasting this continuation directly below the first part:
rankingList.innerHTML = result.evaluatedHospitals.map((hospital) => {
const rankedHospital = rankLookup.get(hospital.id);
if (!rankedHospital) {
return `
<div class="ranking-item rejected">
<strong>Rejected: ${hospital.name}</strong>
<span>${hospital.rejectionReasons.join("; ")}</span>
</div>
`;
}
return `
<div class="ranking-item">
<strong>#${rankedHospital.rank} ${hospital.name}: score ${rankedHospital.score}</strong>
<span>100 - (${hospital.travelTime} × ${acuityDetails[acuity].travelWeight}) + (${hospital.availableBeds} × 4)</span>
</div>
`;
}).join("");
populateOperatorChoices(result.eligibleHospitals);
renderBriefing(requiredSpecialty);
}
How does the renderer explain the result?
- Rejected hospitals display the reasons collected by getEligibleHospitals().
- Eligible hospitals display their rank plus the exact values used in the project formula.
- The renderer sends eligible destinations to the operator controls.
- The renderer also updates the presentation briefing with the selected specialty.
- Save index.html by pressing Ctrl+S.
- Switch back to the Integrated Browser tab.
- Refresh the local preview using its refresh control.
You should see the complete dashboard reload with its transfer controls and hospital network table. This confirms that the recommendation engine plus both renderer parts have valid syntax.
Did the recommendation panel disappear?
Confirm that the renderer continuation sits directly below the first part. The final closing brace must appear after the calls to populateOperatorChoices() and renderBriefing().
Check that the temporary recommendHospital() function was fully replaced. Help me check the recommendation and rendering functions.
Connect the explanation and verify the result
The remaining functions connect the ranked result to the operator controls and briefing. The analysis handler then runs the complete filter-first workflow whenever you select the dashboard button.
- Place your cursor directly below the closing brace of renderRecommendation().
- Add the operator-choice helper by pasting this function:
function populateOperatorChoices(eligibleHospitals) {
const choice = document.getElementById("operatorChoice");
const hasRecommendation = eligibleHospitals.length > 0;
choice.disabled = !hasRecommendation;
document.getElementById("approveButton").disabled = !hasRecommendation;
document.getElementById("overrideButton").disabled = !hasRecommendation;
document.getElementById("capacityButton").disabled = !hasRecommendation;
if (!hasRecommendation) {
choice.innerHTML = "<option>No eligible destination</option>";
return;
}
choice.innerHTML = eligibleHospitals.map((hospital) => `
<option value="${hospital.id}">${hospital.name}</option>
`).join("");
choice.value = currentRecommendation.id;
}
How are operator choices kept safe?
- The operator controls become available only when at least one eligible hospital exists.
- The destination menu contains only hospitals that passed the mandatory checks.
- The recommended hospital becomes the selected option by default.
- Locate the closing brace of the existing analyzeRequest() function.
- Place your cursor directly below that closing brace.
- Add the presentation briefing helper by pasting this function:
function renderBriefing(requiredSpecialty) {
const destination = currentRecommendation
? `${currentRecommendation.name} currently ranks first for ${requiredSpecialty}.`
: `No eligible ${requiredSpecialty} destination is currently available.`;
document.getElementById("briefingList").innerHTML = `
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Exclude hospitals without beds or ${requiredSpecialty}, then apply a visible weighted score.</li>
<li>Current result: ${destination}</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
`;
}
What does the briefing explain?
- The problem statement identifies the weakness in nearest-only routing.
- The method statement names the eligibility-first scoring approach.
- The current result shows the leading destination for the selected specialty.
- The final statements preserve human oversight plus the educational safety boundary.
- Save index.html by pressing Ctrl+S.
- Switch back to the Integrated Browser tab.
- Refresh the local preview using its refresh control.
You should see the dashboard reload with its briefing still visible. This confirms that both supporting functions are ready for the analysis handler.
Did the preview stop loading?
Check that populateOperatorChoices() appears after the renderer. Confirm that renderBriefing() sits inside the existing <script> element.
Look for a missing backtick around either HTML template. Help me inspect these dashboard helper functions.
- Return to the existing analyzeRequest() function.
- Select the entire function from its opening line through its closing brace.
- Replace the selected function by pasting this version:
function analyzeRequest() {
const acuity = document.getElementById("acuity").value;
const specialty = document.getElementById("specialty").value;
const result = recommendHospital(acuity, specialty);
renderRecommendation(result, acuity, specialty);
addLog(`Analyzed a ${acuityDetails[acuity].label} request requiring ${specialty}.`);
document.getElementById("decisionFeedback").textContent = "";
}
How does the analysis flow connect?
- The handler reads the current acuity selection from the transfer form.
- It reads the required specialty from the second selector.
- It passes both values into the filter-first recommendation engine.
- It renders the explanation plus an activity-log entry for the analysis.
- Save index.html by pressing Ctrl+S.
- Switch back to the Integrated Browser tab.
- Refresh the local preview using its refresh control.
Before you test the update, which hospital do you expect to rank first once specialty and capacity checks come before travel time?
- Select RED in the Clinician-assigned acuity field.
- Select Trauma in the Required specialty field.
- Click Analyze request.
You should see Central Trauma Centre become the recommended destination with a score of 66.
Northside Community Hospital should appear as rejected with Does not provide Trauma as its reason. Lakeside Regional Hospital should rank second with a score of 58.
That closes the safety gap you exposed earlier. Every destination now passes the mandatory checks before its score can influence the recommendation.
Still seeing Northside recommended?
Confirm that analyzeRequest() passes both acuity and specialty into recommendHospital().
Check that the old nearest-only function was removed completely. Help me diagnose why an ineligible hospital still reaches the ranking.
✔️ Awesome, I've got everything!
Your eligibility-first routing section is saved. The Integrated Browser now shows a transparent Trauma recommendation.
ⓧ I'd like to double check the full code
function getEligibleHospitals(requiredSpecialty) {
return hospitals.map((hospital) => {
const rejectionReasons = [];
if (hospital.availableBeds <= 0) {
rejectionReasons.push("No available beds");
}
if (!hospital.specialties.includes(requiredSpecialty)) {
rejectionReasons.push(`Does not provide ${requiredSpecialty}`);
}
return {
...hospital,
eligible: rejectionReasons.length === 0,
rejectionReasons
};
});
}
function calculateRoutingScore(hospital, acuity) {
const travelWeight = acuityDetails[acuity].travelWeight;
return 100 - (hospital.travelTime * travelWeight) + (hospital.availableBeds * 4);
}
function recommendHospital(acuity, requiredSpecialty) {
const evaluatedHospitals = getEligibleHospitals(requiredSpecialty);
const eligibleHospitals = evaluatedHospitals
.filter((hospital) => hospital.eligible)
.map((hospital) => ({
...hospital,
score: calculateRoutingScore(hospital, acuity)
}))
.sort((first, second) => second.score - first.score);
return {
evaluatedHospitals,
eligibleHospitals,
recommendation: eligibleHospitals[0] || null
};
}
function renderRecommendation(result, acuity, requiredSpecialty) {
const badge = document.getElementById("acuityBadge");
const title = document.getElementById("recommendationTitle");
const reason = document.getElementById("recommendationReason");
const rankingList = document.getElementById("rankingList");
const rankLookup = new Map(
result.eligibleHospitals.map((hospital, index) => [hospital.id, { ...hospital, rank: index + 1 }])
);
badge.textContent = `${acuityDetails[acuity].label}: ${acuityDetails[acuity].description}`;
badge.className = `badge ${acuity}`;
currentRecommendation = result.recommendation;
currentEligibleHospitals = result.eligibleHospitals;
if (result.recommendation) {
title.textContent = `Recommended destination: ${result.recommendation.name}`;
reason.textContent = `${result.recommendation.name} passed the bed and ${requiredSpecialty} checks, then ranked first with a score of ${result.recommendation.score}.`;
} else {
title.textContent = "No eligible destination in the fictional network";
reason.textContent = `No hospital currently has both an available bed and the required ${requiredSpecialty} capability. Escalation to a human coordinator is required.`;
}
rankingList.innerHTML = result.evaluatedHospitals.map((hospital) => {
const rankedHospital = rankLookup.get(hospital.id);
if (!rankedHospital) {
return `
<div class="ranking-item rejected">
<strong>Rejected: ${hospital.name}</strong>
<span>${hospital.rejectionReasons.join("; ")}</span>
</div>
`;
}
return `
<div class="ranking-item">
<strong>#${rankedHospital.rank} ${hospital.name}: score ${rankedHospital.score}</strong>
<span>100 - (${hospital.travelTime} × ${acuityDetails[acuity].travelWeight}) + (${hospital.availableBeds} × 4)</span>
</div>
`;
}).join("");
populateOperatorChoices(result.eligibleHospitals);
renderBriefing(requiredSpecialty);
}
function populateOperatorChoices(eligibleHospitals) {
const choice = document.getElementById("operatorChoice");
const hasRecommendation = eligibleHospitals.length > 0;
choice.disabled = !hasRecommendation;
document.getElementById("approveButton").disabled = !hasRecommendation;
document.getElementById("overrideButton").disabled = !hasRecommendation;
document.getElementById("capacityButton").disabled = !hasRecommendation;
if (!hasRecommendation) {
choice.innerHTML = "<option>No eligible destination</option>";
return;
}
choice.innerHTML = eligibleHospitals.map((hospital) => `
<option value="${hospital.id}">${hospital.name}</option>
`).join("");
choice.value = currentRecommendation.id;
}
function analyzeRequest() {
const acuity = document.getElementById("acuity").value;
const specialty = document.getElementById("specialty").value;
const result = recommendHospital(acuity, specialty);
renderRecommendation(result, acuity, specialty);
addLog(`Analyzed a ${acuityDetails[acuity].label} request requiring ${specialty}.`);
document.getElementById("decisionFeedback").textContent = "";
}
function renderBriefing(requiredSpecialty) {
const destination = currentRecommendation
? `${currentRecommendation.name} currently ranks first for ${requiredSpecialty}.`
: `No eligible ${requiredSpecialty} destination is currently available.`;
document.getElementById("briefingList").innerHTML = `
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Exclude hospitals without beds or ${requiredSpecialty}, then apply a visible weighted score.</li>
<li>Current result: ${destination}</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
`;
}
Your dashboard now rejects unsuitable destinations before comparing scores. Next, you will give the human operator explicit decision controls plus a way to respond when hospital capacity changes.
Add Decisions and Dynamic Rerouting
The safe ranking now rejects unsuitable hospitals before recommending Central Trauma Centre. The operator can also inspect every score.
A recommendation can still create automation bias when the software appears to make the final choice. An explicit human-in-the-loop decision keeps the operator accountable.
This step also adds capacity-aware rerouting. The dashboard recalculates the route when the recommended hospital loses its available beds.
In this step, get ready to:
- Record operator approvals or overrides in the activity log.
- Recalculate the route after a hospital loses capacity.
- Reset the simulation for a repeatable presentation.
Record operator decisions
The activity log turns each analysis or decision into an auditable event. A timestamp shows when the operator took that action.
- In index.html, find the closing brace of renderBriefing(requiredSpecialty).
- Add the timestamped log helper directly below that function by pasting this code:
function addLog(message) {
const time = new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit"
});
const item = document.createElement("li");
item.textContent = `${time}: ${message}`;
document.getElementById("activityLog").prepend(item);
}
What does this helper do?
- The time value stores a short local timestamp for the event.
- The new list item combines that timestamp with the supplied message.
- The prepend() call places the newest event at the top of the activity log.
- Save index.html.
- Refresh the Integrated Browser preview.
- Click Analyze request.
You will see a timestamped analysis entry at the top of the activity log. The dashboard can now preserve an ordered operational history.
No analysis entry in the log?
Check that addLog(message) sits inside the script. Confirm that it appears before the event-listener lines.
Look for matching braces around the helper. Help me fix the activity log helper.
The metrics need one rendering function that copies the decision state into the three visible cards. Each recorded action can call this helper immediately.
- In index.html, find function renderBriefing(requiredSpecialty) {.
- Add the metrics renderer directly above that function by pasting this code:
function renderMetrics() {
document.getElementById("decisionCount").textContent = metrics.decisions;
document.getElementById("overrideCount").textContent = metrics.overrides;
document.getElementById("eventCount").textContent = metrics.events;
}
How are the metrics updated?
- The first assignment displays the total number of recorded decisions.
- The second assignment displays how many decisions were overrides.
- The final assignment displays how many capacity events occurred.
- Save index.html.
- Refresh the Integrated Browser preview.
The three metric cards remain visible with values of 0. The renderer is ready to replace those values after the first decision.
An approval always uses the current recommendation. An override uses the different eligible destination selected by the operator.
- In index.html, find the closing brace of analyzeRequest().
- Add the decision recorder directly below that function by pasting this code:
function recordDecision(decisionType) {
if (!currentRecommendation) {
return;
}
const selectedId = decisionType === "approve"
? currentRecommendation.id
: document.getElementById("operatorChoice").value;
const selectedHospital = hospitals.find((hospital) => hospital.id === selectedId);
const note = document.getElementById("operatorNote").value.trim();
const feedback = document.getElementById("decisionFeedback");
if (decisionType === "override" && selectedId === currentRecommendation.id) {
feedback.textContent = "Choose a different eligible hospital before recording an override.";
return;
}
metrics.decisions += 1;
if (decisionType === "override") {
metrics.overrides += 1;
}
const action = decisionType === "approve" ? "Approved" : "Overrode recommendation and selected";
const noteText = note ? ` Note: ${note}` : "";
addLog(`${action} ${selectedHospital.name}.${noteText}`);
feedback.textContent = `${action} ${selectedHospital.name}.`;
document.getElementById("operatorNote").value = "";
renderMetrics();
}
How does decision recording work?
- The approval path takes the hospital stored in currentRecommendation.
- The override path reads the hospital selected in operatorChoice.
- The guard rejects an override when the selected hospital still matches the recommendation.
- A valid action updates the counters. It also writes the hospital and optional note to the activity log.
- Scroll to the event-listener section near the bottom of index.html.
- Find this listener:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
What is connected now?
This existing listener only connects the analysis button. The two decision buttons still need their own click handlers.
- Replace that listener with this three-listener group:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
What do the new listeners connect?
- The approval listener passes approve to the shared decision function.
- The override listener passes override to the same function.
- Both paths now share the validation and logging logic.
- Save index.html.
- Refresh the Integrated Browser preview.
- Select RED as the clinician-assigned acuity.
- Select Trauma as the required specialty.
- Click Analyze request.
- Select Lakeside Regional Hospital as the eligible destination.
- Enter Regional capacity balancing drill in the Operator note field.
- Click Record override.
You will see Decisions increase to 1. Overrides also increases to 1.
The feedback confirms the selected hospital. The activity log includes the operator note with a timestamp.
Override not being recorded?
Choose Lakeside Regional Hospital before clicking Record override. Selecting Central Trauma Centre triggers the same-destination guard.
Confirm that the override listener passes override to recordDecision(). Help me debug the operator override.
React to a capacity loss
Hospital availability can change after a recommendation is calculated. The simulation responds by setting the recommended hospital to zero beds before running the same eligibility checks again.
- In index.html, find the closing brace of recordDecision(decisionType).
- Add the capacity-change function directly below it by pasting this code:
function simulateCapacityChange() {
if (!currentRecommendation) {
return;
}
const affectedHospital = hospitals.find(
(hospital) => hospital.id === currentRecommendation.id
);
affectedHospital.availableBeds = 0;
metrics.events += 1;
addLog(`${affectedHospital.name} reported zero available beds. Recalculating the route.`);
renderHospitals();
renderMetrics();
analyzeRequest();
}
How does rerouting happen?
- The function finds the hospital represented by currentRecommendation.
- Its availableBeds value becomes 0. The eligibility check rejects that hospital during recalculation.
- The event counter records the simulated capacity change.
- The final analyzeRequest() call produces a replacement recommendation.
- Return to the event-listener group near the bottom of index.html.
- Find this group:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
Which controls are active?
Analysis and human decision controls are connected. The capacity button does not yet call its simulation function.
- Replace that group with this four-listener version:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
document.getElementById("capacityButton").addEventListener("click", simulateCapacityChange);
What changed in the listener group?
The capacityButton listener now runs simulateCapacityChange(). The button becomes available after an eligible recommendation exists.
Before you test the capacity event, predict which Trauma hospital will rank first after Central Trauma Centre drops to zero beds.
- Save index.html.
- Refresh the Integrated Browser preview.
- Click Analyze request with the default request selected.
- Click Simulate capacity loss.
Central Trauma Centre now shows 0 available beds. Lakeside Regional Hospital becomes the recommended eligible Trauma destination.
Capacity events increases to 1. The activity log records the loss before the recalculated analysis entry.
Route not changing to Lakeside?
Confirm that you analysed the default Trauma request before simulating the event. The capacity button depends on an existing recommendation.
Check that affectedHospital.availableBeds = 0 appears before analyzeRequest(). Help me diagnose the rerouting result.
Reset and verify the complete workflow
A presentation needs a repeatable starting state. The reset function restores the original beds and clears every simulation counter.
- In index.html, find the closing brace of simulateCapacityChange().
- Add the first reset layer directly below that function by pasting this code:
function resetSimulation() {
hospitals.forEach((hospital) => {
hospital.availableBeds = hospital.initialBeds;
});
metrics.decisions = 0;
metrics.overrides = 0;
metrics.events = 0;
currentRecommendation = null;
currentEligibleHospitals = [];
renderHospitals();
renderMetrics();
}
What does the first reset layer restore?
- Each hospital receives its original bed count from initialBeds.
- All three metric values return to 0.
- The stored recommendation and eligible-hospital list are cleared.
- The hospital table and metric cards are rendered from the restored state.
- Return to the listener group near the bottom of index.html.
- Find this section:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
document.getElementById("capacityButton").addEventListener("click", simulateCapacityChange);
renderHospitals();
What is missing from startup?
The current section connects four buttons. It renders the hospital table when the page loads.
- Replace that section with the completed startup group:
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
document.getElementById("capacityButton").addEventListener("click", simulateCapacityChange);
document.getElementById("resetButton").addEventListener("click", resetSimulation);
renderHospitals();
renderMetrics();
What does the completed startup group do?
- The reset listener connects resetButton to resetSimulation().
- The final two calls display the initial hospital data and metric values as soon as the page loads.
- Save index.html.
- Refresh the Integrated Browser preview.
- Click Analyze request.
- Click Simulate capacity loss.
- Click Reset simulation.
Central Trauma Centre returns to 2 beds. All three metric cards return to 0.
The reset also needs to restore the form controls and recommendation panel. This gives every demonstration the same visible starting point.
- Inside resetSimulation(), find currentEligibleHospitals = [];.
- Add the interface reset directly below that line by pasting this code:
document.getElementById("acuity").value = "red";
document.getElementById("specialty").value = "Trauma";
document.getElementById("acuityBadge").textContent = "Awaiting request";
document.getElementById("acuityBadge").className = "badge";
document.getElementById("recommendationTitle").textContent = "No destination recommendation yet";
document.getElementById("recommendationReason").textContent = "Select the synthetic request details and analyze the network.";
document.getElementById("rankingList").innerHTML = "";
document.getElementById("operatorChoice").innerHTML = "<option>Analyze a request first</option>";
document.getElementById("operatorChoice").disabled = true;
document.getElementById("approveButton").disabled = true;
document.getElementById("overrideButton").disabled = true;
document.getElementById("capacityButton").disabled = true;
What does the interface reset cover?
- The request returns to the default RED Trauma scenario.
- The recommendation panel returns to its waiting state.
- The ranking list is cleared.
- Decision controls become unavailable until the operator analyses another request.
- Save index.html.
- Refresh the Integrated Browser preview.
- Click Analyze request.
- Click Reset simulation.
The recommendation title returns to No destination recommendation yet. The operator controls become disabled again.
The final reset layer clears operator feedback and restores the original briefing. It also removes earlier log entries from the next demonstration.
- Inside resetSimulation(), find the line that disables capacityButton.
- Add the final reset layer directly below that line by pasting this code:
document.getElementById("decisionFeedback").textContent = "Simulation reset.";
document.getElementById("operatorNote").value = "";
document.getElementById("activityLog").innerHTML = "<li>Simulation ready. No patient data has been loaded.</li>";
document.getElementById("briefingList").innerHTML = `
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Apply mandatory eligibility checks, then rank suitable fictional hospitals.</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
`;
Why restore the log and briefing?
- The feedback confirms that the reset action completed.
- The operator note is cleared before the next scenario.
- The activity log returns to one safe initial message.
- The briefing again states the problem and human role. It also preserves the educational safety boundary.
✔️ Awesome, I've got everything!
Great. Double-check that you saved index.html before running the final workflow.
ⓧ I'd like to double check the full code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emergency Routing Control Centre</title>
<style>
:root {
--navy: #10233f;
--blue: #1769aa;
--light-blue: #eaf4ff;
--green: #16794b;
--yellow: #b26a00;
--red: #b42318;
--ink: #172033;
--muted: #5d687a;
--line: #d9e0ea;
--surface: #ffffff;
--background: #f3f6fa;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
color: var(--ink);
background: var(--background);
}
button,
select,
textarea {
font: inherit;
}
.topbar {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: center;
padding: 24px clamp(20px, 5vw, 64px);
color: white;
background: linear-gradient(120deg, var(--navy), #174b76);
}
.eyebrow {
margin: 0 0 6px;
color: #a8d7ff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
margin-bottom: 8px;
font-size: clamp(1.7rem, 4vw, 2.6rem);
}
.topbar p:last-child {
margin-bottom: 0;
color: #dceeff;
}
.network-status {
min-width: 210px;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
font-weight: 700;
text-align: center;
}
.pulse {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 8px;
border-radius: 50%;
background: #56e39f;
}
main {
width: min(1180px, calc(100% - 32px));
margin: 24px auto 48px;
}
.notice {
margin-bottom: 18px;
padding: 14px 16px;
border-left: 5px solid var(--yellow);
border-radius: 8px;
background: #fff7e8;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.card {
padding: 20px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 8px 24px rgba(16, 35, 63, 0.06);
}
.wide {
grid-column: 1 / -1;
}
label {
display: block;
margin: 14px 0 6px;
font-size: 0.9rem;
font-weight: 700;
}
select,
textarea {
width: 100%;
padding: 11px 12px;
border: 1px solid #b9c4d2;
border-radius: 8px;
background: white;
}
textarea {
min-height: 82px;
resize: vertical;
}
.button-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
button {
padding: 10px 14px;
border: 0;
border-radius: 8px;
color: white;
background: var(--blue);
cursor: pointer;
font-weight: 700;
}
button.secondary {
color: var(--navy);
background: #dcecff;
}
button.warning {
background: var(--yellow);
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th,
td {
padding: 10px 8px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
th {
color: var(--muted);
font-size: 0.78rem;
text-transform: uppercase;
}
.badge {
display: inline-block;
margin-bottom: 10px;
padding: 6px 10px;
border-radius: 999px;
color: white;
background: var(--muted);
font-size: 0.8rem;
font-weight: 800;
}
.badge.red {
background: var(--red);
}
.badge.yellow {
color: #332000;
background: #ffd166;
}
.badge.green {
background: var(--green);
}
.formula {
padding: 12px;
border-radius: 8px;
color: #0d426d;
background: var(--light-blue);
font-family: Consolas, monospace;
}
.ranking-list {
display: grid;
gap: 10px;
margin-top: 14px;
}
.ranking-item {
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
}
.ranking-item.rejected {
border-color: #f1b8b4;
background: #fff2f1;
}
.ranking-item strong {
display: block;
margin-bottom: 5px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.metric {
padding: 14px;
border-radius: 10px;
background: var(--light-blue);
text-align: center;
}
.metric span {
display: block;
color: var(--muted);
font-size: 0.78rem;
}
.metric strong {
display: block;
margin-top: 4px;
font-size: 1.55rem;
}
#activityLog {
max-height: 220px;
margin-bottom: 0;
padding-left: 22px;
overflow: auto;
}
#activityLog li {
margin-bottom: 8px;
}
.feedback {
min-height: 24px;
margin: 12px 0 0;
color: var(--green);
font-weight: 700;
}
.briefing {
padding: 16px;
border-radius: 10px;
background: #f7f9fc;
}
.briefing li {
margin-bottom: 8px;
}
.muted {
color: var(--muted);
}
@media (max-width: 780px) {
.topbar {
align-items: flex-start;
flex-direction: column;
}
.grid {
grid-template-columns: 1fr;
}
.wide {
grid-column: auto;
}
.metric-grid {
grid-template-columns: 1fr;
}
table {
font-size: 0.8rem;
}
}
</style>
</head>
<body>
<header class="topbar">
<div>
<p class="eyebrow">Regional Operations Simulation</p>
<h1>Emergency Routing Control Centre</h1>
<p>Explainable destination support for a human control-centre operator</p>
</div>
<div class="network-status"><span class="pulse"></span>Simulation network active</div>
</header>
<main>
<section class="notice">
<strong>Educational simulation only.</strong> All patients, hospitals, capacities, travel times, and scores are fictional. This page must not be used for diagnosis, clinical triage, dispatch, or real patient care.
</section>
<div class="grid">
<section class="card">
<h2>Synthetic transfer request</h2>
<p class="muted">The acuity category is assumed to have been assigned by trained clinical staff.</p>
<label for="acuity">Clinician-assigned acuity</label>
<select id="acuity">
<option value="red">RED: High acuity; need to be seen immediately</option>
<option value="yellow">YELLOW: Moderate acuity; need to be seen soon</option>
<option value="green">GREEN: Low acuity; can wait</option>
</select>
<label for="specialty">Required specialty</label>
<select id="specialty">
<option value="Trauma">Trauma</option>
<option value="Cardiac">Cardiac</option>
<option value="General">General</option>
</select>
<div class="button-row">
<button id="analyzeButton" type="button">Analyze request</button>
<button id="resetButton" class="secondary" type="button">Reset simulation</button>
</div>
</section>
<section class="card">
<h2>Fictional hospital network</h2>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Hospital</th>
<th>Travel</th>
<th>Beds</th>
<th>Specialties</th>
</tr>
</thead>
<tbody id="hospitalRows"></tbody>
</table>
</div>
</section>
<section class="card wide">
<span id="acuityBadge" class="badge">Awaiting request</span>
<h2 id="recommendationTitle">No destination recommendation yet</h2>
<p id="recommendationReason" class="muted">Select the synthetic request details and analyze the network.</p>
<p class="formula">Project formula: 100 - (travel time × acuity weight) + (available beds × 4). Higher scores rank first after mandatory eligibility checks.</p>
<div id="rankingList" class="ranking-list"></div>
</section>
<section class="card">
<h2>Human control-centre decision</h2>
<label for="operatorChoice">Eligible destination</label>
<select id="operatorChoice" disabled>
<option>Analyze a request first</option>
</select>
<label for="operatorNote">Operator note</label>
<textarea id="operatorNote" placeholder="Optional operational reason for approval or override"></textarea>
<div class="button-row">
<button id="approveButton" type="button" disabled>Approve recommendation</button>
<button id="overrideButton" class="warning" type="button" disabled>Record override</button>
</div>
<p id="decisionFeedback" class="feedback" aria-live="polite"></p>
</section>
<section class="card">
<h2>Control-centre metrics</h2>
<div class="metric-grid">
<div class="metric"><span>Decisions</span><strong id="decisionCount">0</strong></div>
<div class="metric"><span>Overrides</span><strong id="overrideCount">0</strong></div>
<div class="metric"><span>Capacity events</span><strong id="eventCount">0</strong></div>
</div>
<button id="capacityButton" class="warning" type="button" disabled>Simulate capacity loss</button>
</section>
<section class="card wide">
<h2>Activity log</h2>
<ol id="activityLog">
<li>Simulation ready. No patient data has been loaded.</li>
</ol>
</section>
<section class="card wide">
<h2>Presentation briefing</h2>
<div class="briefing">
<ul id="briefingList">
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Apply mandatory eligibility checks, then rank suitable fictional hospitals.</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
</ul>
</div>
</section>
</div>
</main>
<script>
const hospitals = [
{
id: "northside",
name: "Northside Community Hospital",
travelTime: 6,
availableBeds: 5,
initialBeds: 5,
specialties: ["General", "Cardiac"]
},
{
id: "central",
name: "Central Trauma Centre",
travelTime: 14,
availableBeds: 2,
initialBeds: 2,
specialties: ["General", "Trauma", "Cardiac"]
},
{
id: "lakeside",
name: "Lakeside Regional Hospital",
travelTime: 22,
availableBeds: 6,
initialBeds: 6,
specialties: ["General", "Trauma"]
}
];
const acuityDetails = {
red: {
label: "RED",
description: "High acuity; need to be seen immediately",
travelWeight: 3
},
yellow: {
label: "YELLOW",
description: "Moderate acuity; need to be seen soon",
travelWeight: 2
},
green: {
label: "GREEN",
description: "Low acuity; can wait",
travelWeight: 1
}
};
const metrics = {
decisions: 0,
overrides: 0,
events: 0
};
let currentRecommendation = null;
let currentEligibleHospitals = [];
function renderHospitals() {
const rows = hospitals.map((hospital) => `
<tr>
<td><strong>${hospital.name}</strong></td>
<td>${hospital.travelTime} min</td>
<td>${hospital.availableBeds}</td>
<td>${hospital.specialties.join(", ")}</td>
</tr>
`).join("");
document.getElementById("hospitalRows").innerHTML = rows;
}
function getEligibleHospitals(requiredSpecialty) {
return hospitals.map((hospital) => {
const rejectionReasons = [];
if (hospital.availableBeds <= 0) {
rejectionReasons.push("No available beds");
}
if (!hospital.specialties.includes(requiredSpecialty)) {
rejectionReasons.push(`Does not provide ${requiredSpecialty}`);
}
return {
...hospital,
eligible: rejectionReasons.length === 0,
rejectionReasons
};
});
}
function calculateRoutingScore(hospital, acuity) {
const travelWeight = acuityDetails[acuity].travelWeight;
return 100 - (hospital.travelTime * travelWeight) + (hospital.availableBeds * 4);
}
function recommendHospital(acuity, requiredSpecialty) {
const evaluatedHospitals = getEligibleHospitals(requiredSpecialty);
const eligibleHospitals = evaluatedHospitals
.filter((hospital) => hospital.eligible)
.map((hospital) => ({
...hospital,
score: calculateRoutingScore(hospital, acuity)
}))
.sort((first, second) => second.score - first.score);
return {
evaluatedHospitals,
eligibleHospitals,
recommendation: eligibleHospitals[0] || null
};
}
function renderRecommendation(result, acuity, requiredSpecialty) {
const badge = document.getElementById("acuityBadge");
const title = document.getElementById("recommendationTitle");
const reason = document.getElementById("recommendationReason");
const rankingList = document.getElementById("rankingList");
const rankLookup = new Map(
result.eligibleHospitals.map((hospital, index) => [hospital.id, { ...hospital, rank: index + 1 }])
);
badge.textContent = `${acuityDetails[acuity].label}: ${acuityDetails[acuity].description}`;
badge.className = `badge ${acuity}`;
currentRecommendation = result.recommendation;
currentEligibleHospitals = result.eligibleHospitals;
if (result.recommendation) {
title.textContent = `Recommended destination: ${result.recommendation.name}`;
reason.textContent = `${result.recommendation.name} passed the bed and ${requiredSpecialty} checks, then ranked first with a score of ${result.recommendation.score}.`;
} else {
title.textContent = "No eligible destination in the fictional network";
reason.textContent = `No hospital currently has both an available bed and the required ${requiredSpecialty} capability. Escalation to a human coordinator is required.`;
}
rankingList.innerHTML = result.evaluatedHospitals.map((hospital) => {
const rankedHospital = rankLookup.get(hospital.id);
if (!rankedHospital) {
return `
<div class="ranking-item rejected">
<strong>Rejected: ${hospital.name}</strong>
<span>${hospital.rejectionReasons.join("; ")}</span>
</div>
`;
}
return `
<div class="ranking-item">
<strong>#${rankedHospital.rank} ${hospital.name}: score ${rankedHospital.score}</strong>
<span>100 - (${hospital.travelTime} × ${acuityDetails[acuity].travelWeight}) + (${hospital.availableBeds} × 4)</span>
</div>
`;
}).join("");
populateOperatorChoices(result.eligibleHospitals);
renderBriefing(requiredSpecialty);
}
function populateOperatorChoices(eligibleHospitals) {
const choice = document.getElementById("operatorChoice");
const hasRecommendation = eligibleHospitals.length > 0;
choice.disabled = !hasRecommendation;
document.getElementById("approveButton").disabled = !hasRecommendation;
document.getElementById("overrideButton").disabled = !hasRecommendation;
document.getElementById("capacityButton").disabled = !hasRecommendation;
if (!hasRecommendation) {
choice.innerHTML = "<option>No eligible destination</option>";
return;
}
choice.innerHTML = eligibleHospitals.map((hospital) => `
<option value="${hospital.id}">${hospital.name}</option>
`).join("");
choice.value = currentRecommendation.id;
}
function analyzeRequest() {
const acuity = document.getElementById("acuity").value;
const specialty = document.getElementById("specialty").value;
const result = recommendHospital(acuity, specialty);
renderRecommendation(result, acuity, specialty);
addLog(`Analyzed a ${acuityDetails[acuity].label} request requiring ${specialty}.`);
document.getElementById("decisionFeedback").textContent = "";
}
function recordDecision(decisionType) {
if (!currentRecommendation) {
return;
}
const selectedId = decisionType === "approve"
? currentRecommendation.id
: document.getElementById("operatorChoice").value;
const selectedHospital = hospitals.find((hospital) => hospital.id === selectedId);
const note = document.getElementById("operatorNote").value.trim();
const feedback = document.getElementById("decisionFeedback");
if (decisionType === "override" && selectedId === currentRecommendation.id) {
feedback.textContent = "Choose a different eligible hospital before recording an override.";
return;
}
metrics.decisions += 1;
if (decisionType === "override") {
metrics.overrides += 1;
}
const action = decisionType === "approve" ? "Approved" : "Overrode recommendation and selected";
const noteText = note ? ` Note: ${note}` : "";
addLog(`${action} ${selectedHospital.name}.${noteText}`);
feedback.textContent = `${action} ${selectedHospital.name}.`;
document.getElementById("operatorNote").value = "";
renderMetrics();
}
function simulateCapacityChange() {
if (!currentRecommendation) {
return;
}
const affectedHospital = hospitals.find(
(hospital) => hospital.id === currentRecommendation.id
);
affectedHospital.availableBeds = 0;
metrics.events += 1;
addLog(`${affectedHospital.name} reported zero available beds. Recalculating the route.`);
renderHospitals();
renderMetrics();
analyzeRequest();
}
function resetSimulation() {
hospitals.forEach((hospital) => {
hospital.availableBeds = hospital.initialBeds;
});
metrics.decisions = 0;
metrics.overrides = 0;
metrics.events = 0;
currentRecommendation = null;
currentEligibleHospitals = [];
document.getElementById("acuity").value = "red";
document.getElementById("specialty").value = "Trauma";
document.getElementById("acuityBadge").textContent = "Awaiting request";
document.getElementById("acuityBadge").className = "badge";
document.getElementById("recommendationTitle").textContent = "No destination recommendation yet";
document.getElementById("recommendationReason").textContent = "Select the synthetic request details and analyze the network.";
document.getElementById("rankingList").innerHTML = "";
document.getElementById("operatorChoice").innerHTML = "<option>Analyze a request first</option>";
document.getElementById("operatorChoice").disabled = true;
document.getElementById("approveButton").disabled = true;
document.getElementById("overrideButton").disabled = true;
document.getElementById("capacityButton").disabled = true;
document.getElementById("decisionFeedback").textContent = "Simulation reset.";
document.getElementById("operatorNote").value = "";
document.getElementById("activityLog").innerHTML = "<li>Simulation ready. No patient data has been loaded.</li>";
document.getElementById("briefingList").innerHTML = `
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Apply mandatory eligibility checks, then rank suitable fictional hospitals.</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
`;
renderHospitals();
renderMetrics();
}
function renderMetrics() {
document.getElementById("decisionCount").textContent = metrics.decisions;
document.getElementById("overrideCount").textContent = metrics.overrides;
document.getElementById("eventCount").textContent = metrics.events;
}
function renderBriefing(requiredSpecialty) {
const destination = currentRecommendation
? `${currentRecommendation.name} currently ranks first for ${requiredSpecialty}.`
: `No eligible ${requiredSpecialty} destination is currently available.`;
document.getElementById("briefingList").innerHTML = `
<li>Problem: The nearest hospital may not have the required capability or capacity.</li>
<li>Method: Exclude hospitals without beds or ${requiredSpecialty}, then apply a visible weighted score.</li>
<li>Current result: ${destination}</li>
<li>Human role: A control-centre operator approves or overrides every recommendation.</li>
<li>Safety boundary: This is a rule-based educational MVP using synthetic data, not validated clinical AI.</li>
`;
}
function addLog(message) {
const time = new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit"
});
const item = document.createElement("li");
item.textContent = `${time}: ${message}`;
document.getElementById("activityLog").prepend(item);
}
document.getElementById("analyzeButton").addEventListener("click", analyzeRequest);
document.getElementById("approveButton").addEventListener("click", () => recordDecision("approve"));
document.getElementById("overrideButton").addEventListener("click", () => recordDecision("override"));
document.getElementById("capacityButton").addEventListener("click", simulateCapacityChange);
document.getElementById("resetButton").addEventListener("click", resetSimulation);
renderHospitals();
renderMetrics();
</script>
</body>
</html>
Before the final check, predict how the recommendation and metrics will change after capacity loss followed by an approval.
- Save index.html.
- Refresh the Integrated Browser preview.
- Click Analyze request for the default RED Trauma request.
- Click Simulate capacity loss.
- Click Approve recommendation.
You will see Central Trauma Centre at 0 beds. Lakeside Regional Hospital becomes the recommendation.
Decisions shows 1. Overrides shows 0. Capacity events shows 1.
The activity log contains the capacity event and the approval. The presentation briefing names Lakeside as the current result while preserving the rule-based educational safety boundary.
Final workflow not matching?
Click Reset simulation before repeating the final check. This restores Central Trauma Centre to 2 beds and clears all counters.
Confirm that the final listener section includes all five buttons. Help me trace the complete decision workflow.
That completes the control-centre workflow. Your fictional routing simulation now combines transparent ranking with human approval and dynamic capacity response.
Secret mission
Run a Regional Capacity-Crisis Drill
Compare three fictional transfer scenarios. Defend a human override during a capacity loss. Finish with an auditable log and a one-minute safety briefing.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local prototype available, pause your work, or remove it entirely. This project runs entirely on your computer, so it creates no cloud resources or ongoing costs.
Resources you used:
- One local er-control-centre folder containing the saved index.html application.
Keep everything running
Keep the local project available if you plan to demonstrate or extend it. The simulation uses only files on your computer.
- Leave the Integrated Browser preview open in Visual Studio Code.
- Keep the er-control-centre folder on your computer.
Pause - I'll come back to this later
Pausing closes the local preview and editor while preserving every line of the project. Your saved application remains ready for another session.
- Close the Integrated Browser tab in Visual Studio Code.
- Close Visual Studio Code.
The saved er-control-centre/index.html file remains ready for you to reopen later.
Delete - I don't want to use this again
Deleting the folder removes the only project resource. This affects only the local simulation because no cloud service, account, patient record, or paid service was created.
- Close the Integrated Browser tab in Visual Studio Code.
- Close Visual Studio Code.
- Press the Windows key to open the search bar.
- Type File Explorer into the search bar.
- Press Enter to open File Explorer.
- Locate the er-control-centre folder you created earlier.
- Select the er-control-centre folder.
- Press the Delete key.
You should no longer see the er-control-centre folder in File Explorer. Its index.html file is removed with it.
Nice Work!
Nice Work!
You did it! Your explainable emergency routing control centre now ranks fictional hospitals using synthetic data.
You've learned how to:
- Build a regional routing dashboard that compares three fictional hospitals for a synthetic transfer request.
- Identify a nearest-hospital failure when the closest destination lacks Trauma. Apply mandatory eligibility checks for beds plus specialty capability. Rank suitable hospitals with a transparent weighted routing score.
- Operate a human-in-the-loop workflow through approval or override actions. Trigger capacity-aware rerouting when a hospital loses beds. Defend each result through live metrics plus an auditable activity log.
- Secret Mission: Run a regional capacity-crisis drill across three scenarios. Defend a human override with operational evidence. Brief the capacity event within the simulation's educational safety boundary.
Ready to quiz yourself?