From bf1b437f772946292c9594649f19abd9e1a6fd95 Mon Sep 17 00:00:00 2001 From: Bryan Lee Date: Sun, 22 Feb 2026 22:08:09 -0500 Subject: [PATCH 1/2] Created a database for the hospitals and displayed them on frontend --- frontend/src/assets/hospital_map_icon.png | Bin 0 -> 1692 bytes frontend/src/components/Dashboard.tsx | 13 ++++++++++++- frontend/src/components/MapPanel.tsx | 20 +++++++++++++++++++- frontend/src/components/types.ts | 7 +++++++ infrastructure/postgres/init.sql | 8 ++++++++ services/dashboard-api/main.py | 16 ++++++++++++++++ 6 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/hospital_map_icon.png diff --git a/frontend/src/assets/hospital_map_icon.png b/frontend/src/assets/hospital_map_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d127f19ba2b82343d18b3669bcb73093c9319fc1 GIT binary patch literal 1692 zcmaKtd0Y|(AI8y?5K%MnnkdexL9xPP-k_3*;)O@%(eTQw%{)dPWa>~}MbTm}ZfW3| zWqIZ`Q%Dq#G|ht!j};otOj9gStIcxmul@7B`+UC7@A>?mf1f{|0+KI54g3Wd005|Y zdbkDbdD7nk0r&3k;-=T0sU&%XQUCx|t-qxJxW?7n3l%5<1RS7w0=BaED8;$@xB>vJ zS5$YRlmP&sg{PZqP`bi;xpO=W4;>bSde9}tX1~OrWlgv!n3^DsJxGzZ)*%n(iW}T) zw3R(pL}nI-cqpD!TUhLKTnip>7=Z&LP`WBaCAAMq&}0hZj&}`&7mJ5$oBISU zpMMfEem6hAithG&^jdKT2O84naZK5z$?;TPlPDle7=te)#uVp z4xWUo6M~t(-Fh(Q0o%vgWkDv08h@Jb-=Tg%6Lo3mQT6p4AP|@vvM~|B6zogePfxfGX&Bc&`7 zHt5J>w4sl4QA&8UB7r=QYR2X1&c1E{!buI^iJCffXA?OK`!V7#D{1K~-Ql>eGM#Z1_ zZb;9&;_fHpzZ=E~gZu)F>BK{(ORN zlyaXEdr5AeTRbUPpk8=D@sL$R2S@(MwQYNpz-P7WLXM?t**!MFJcU)Scl7)ESMp~* zYgg!S-0I;+*Vg18EAGo?UV_rLTrW92glr8MhzqBP!>;mLo<7-Xj?a-4Y+$B`0L=4! zjTLMfHvRgPw>RUq#mb7q^z9ArwvyO{i_B)<8f zd-NWLpChAbAKRjuSB4&V|3m!is{2vdy_!evuodn#En<5*TPzPCSrj#%{fXi~qYV#h z{*)3aE`ncndP(Yk!p-WV<$h#)8?@7vx96E!BQ;08Ttuo1mi4C!GM10^3Nqnb@NlLV zlQ^VZO=X{%KY!4){dyZqZ+z3+JdX)kOARle72OokuPgrNUwG*g8GKbe17P|-%UPu& zJlJn^KwZ{RF7a9XVIpVgR92g=nT#v@>zKg}M7+;OGT~w3- z4Pw6qteJn;MKIZr%eJQ{mOFb8-A0_2(VJq23+$9>yPYqYsZyb@^l|V)ha?8Ro-ih$ zO_mACxup$W88O!=MtD^7;)%#M=u>h@DdgSTOsSZoIa&biEX)9*&mvcaTRMZM`wwug zS6@-uF{C*zy>NG0mPm=M3x@u$Z*`E#Np&b|t^Zr%Kh1(0kN-r(SOUAMMg}FRzklYb z;7z5k8t}WHn4yP~)e5awXM8zxw`^jc#?z6A8g!p^woT2{JTj1y_97Nxa!2P8|IZJz zwb}lkgyf{9RVt!CTJ0#$K37xDXd_k(KM+ta$=?2QS)6YuNRjEl_q3z>QhAOHXW literal 0 HcmV?d00001 diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index c983e4f..5bcd4c0 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,4 +1,5 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; +import { Hospital } from "./types"; import TranscriptPanel from "./TranscriptPanel"; import MapPanel from "./MapPanel"; import "./Dashboard.css"; @@ -49,6 +50,15 @@ const Dashboard = () => { declineAndReassign, } = useDispatchSuggestion(); + const [hospitals, setHospitals] = useState([]); + const apiUrl = import.meta.env.VITE_API_URL || "http://localhost:8000"; + useEffect(() => { + fetch(`${apiUrl}/hospitals`) + .then((res) => res.json()) + .then((data) => setHospitals(data)) + .catch((err) => console.error("Failed to fetch hospitals:", err)); + }, [apiUrl]); + // Server-side auto-dispatch handles new incidents now — no client-side callback needed const { incidents } = useIncidents(); const activeIncidents = incidents.filter((i) => i.status !== "resolved"); @@ -161,6 +171,7 @@ const Dashboard = () => { focusedUnit={focusedUnit} routes={routes} incidents={activeIncidents} + hospitals={hospitals} dispatchSuggestion={suggestion} onAcceptSuggestion={accept} onDeclineSuggestion={declineAndReassign} diff --git a/frontend/src/components/MapPanel.tsx b/frontend/src/components/MapPanel.tsx index a0b7317..8b7b36e 100644 --- a/frontend/src/components/MapPanel.tsx +++ b/frontend/src/components/MapPanel.tsx @@ -10,7 +10,8 @@ import "./MapPanel.css"; import { AmbulanceMapIcon } from "./AmbulanceMapIcon"; import { IncidentMapIcon } from "./IncidentMapIcon"; import { UnitInfo } from "./AmbulancePanel"; -import { CaseInfo, CasePriority, DispatchSuggestion } from "./types"; +import { CaseInfo, CasePriority, DispatchSuggestion, Hospital } from "./types"; +import hospitalIcon from "../assets/hospital_map_icon.png"; const getPriorityColor = (p: string) => { switch (p) { @@ -34,6 +35,7 @@ interface MapPanelProps { focusedUnit: UnitInfo | null; routes?: Array<[string, google.maps.LatLngLiteral[]]>; incidents?: CaseInfo[]; + hospitals?: Hospital[]; dispatchSuggestion?: DispatchSuggestion | null; onAcceptSuggestion?: () => void; onDeclineSuggestion?: () => void; @@ -97,6 +99,7 @@ export default function MapPanel({ focusedUnit, routes = [], incidents = [], + hospitals = [], dispatchSuggestion, onAcceptSuggestion, onDeclineSuggestion, @@ -423,6 +426,21 @@ export default function MapPanel({ ))} + {hospitals.map((hospital) => ( + +
+ {hospital.name} +
+
+ ))} ); diff --git a/frontend/src/components/types.ts b/frontend/src/components/types.ts index 5e0fd74..005c1f5 100644 --- a/frontend/src/components/types.ts +++ b/frontend/src/components/types.ts @@ -15,6 +15,13 @@ export interface CaseInfo { updated_at: string; } +export interface Hospital { + id: number; + name: string; + lat: number; + lon: number; +} + export interface DispatchSuggestion { suggestionId: string; vehicleId: string; diff --git a/infrastructure/postgres/init.sql b/infrastructure/postgres/init.sql index acf5501..1f5ef65 100644 --- a/infrastructure/postgres/init.sql +++ b/infrastructure/postgres/init.sql @@ -61,3 +61,11 @@ CREATE TABLE IF NOT EXISTS incidents ( CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status); CREATE INDEX IF NOT EXISTS idx_incidents_session_id ON incidents(session_id); +-- Hospitals table +CREATE TABLE IF NOT EXISTS hospitals ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + lat DECIMAL(10, 8) NOT NULL, + lon DECIMAL(11, 8) NOT NULL +); + diff --git a/services/dashboard-api/main.py b/services/dashboard-api/main.py index 6ae04a2..15bd7d8 100644 --- a/services/dashboard-api/main.py +++ b/services/dashboard-api/main.py @@ -589,6 +589,22 @@ async def geocode_address(req: GeocodeRequest): # --- ACR Code & Suggestion Accept/Dismiss endpoints --- +@app.get("/hospitals") +async def list_hospitals(): + """Return all hospitals.""" + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute("SELECT id, name, lat, lon FROM hospitals ORDER BY name") + rows = cur.fetchall() + return [ + {"id": r[0], "name": r[1], "lat": float(r[2]), "lon": float(r[3])} + for r in rows + ] + finally: + conn.close() + + @app.get("/acr-codes") async def list_acr_codes(): """Return all Ontario ACR Problem Codes for frontend dropdowns.""" From adc9ca2e0e3286326b79545495a3145f69e88cea Mon Sep 17 00:00:00 2001 From: Bryan Lee Date: Thu, 26 Feb 2026 16:00:52 -0500 Subject: [PATCH 2/2] Quality of life fixes --- .claude/settings.local.json | 5 ++- frontend/src/components/AmbulancePanel.tsx | 25 +++++++++-- frontend/src/components/CasePanel.tsx | 27 +++++++++-- frontend/src/components/Dashboard.tsx | 20 +++++++++ frontend/src/components/MapPanel.tsx | 52 +++++++++++++++++++--- 5 files changed, 116 insertions(+), 13 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 453bf7e..c94ccdf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,10 @@ { "permissions": { "allow": [ - "Bash(docker compose:*)" + "Bash(docker compose:*)", + "Bash(curl:*)", + "Bash(python3:*)", + "WebSearch" ] } } diff --git a/frontend/src/components/AmbulancePanel.tsx b/frontend/src/components/AmbulancePanel.tsx index a4f7372..33b7305 100644 --- a/frontend/src/components/AmbulancePanel.tsx +++ b/frontend/src/components/AmbulancePanel.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useMemo, useEffect, useRef } from "react"; import "./AmbulancePanel.css"; import AmbulanceCard from "./AmbulanceCard"; @@ -26,6 +26,7 @@ interface PanelProps { handleViewChange: (view: ActiveView) => void; units: UnitInfo[]; onUnitClick: (unit: UnitInfo) => void; + focusedUnitId?: string | null; } const STATUS_FILTER_ORDER: { key: UnitStatus; cssClass: string }[] = [ @@ -40,7 +41,25 @@ export default function AmbulancePanel({ handleViewChange, units, onUnitClick, + focusedUnitId, }: PanelProps) { + const unitListRef = useRef(null); + + const sortedUnits = useMemo(() => { + if (!focusedUnitId) return units; + return [...units].sort((a, b) => { + if (a.id === focusedUnitId) return -1; + if (b.id === focusedUnitId) return 1; + return 0; + }); + }, [units, focusedUnitId]); + + useEffect(() => { + if (focusedUnitId && unitListRef.current) { + unitListRef.current.scrollTop = 0; + } + }, [focusedUnitId]); + const statusCounts = useMemo(() => { const counts: Record = {}; for (const u of units) { @@ -72,8 +91,8 @@ export default function AmbulancePanel({

Active Units

{/* Unit List */} -
- {units.map((u) => ( +
+ {sortedUnits.map((u) => ( // Wrap the card in a div to handle the click
void; dispatchLoading?: boolean; dispatchInfoMap?: Record; + focusedIncidentId?: string | null; } export default function CasesPanel({ @@ -35,7 +37,26 @@ export default function CasesPanel({ onDispatch, dispatchLoading, dispatchInfoMap = {}, + focusedIncidentId, }: PanelProps): JSX.Element { + const caseListRef = useRef(null); + + // Sort incidents so the focused one is at the top + const sortedIncidents = useMemo(() => { + if (!focusedIncidentId) return incidents; + return [...incidents].sort((a, b) => { + if (a.id === focusedIncidentId) return -1; + if (b.id === focusedIncidentId) return 1; + return 0; + }); + }, [incidents, focusedIncidentId]); + + // Scroll the list container to the top when a focused incident changes + useEffect(() => { + if (focusedIncidentId && caseListRef.current) { + caseListRef.current.scrollTop = 0; + } + }, [focusedIncidentId]); const priorityCounts: PriorityCounts = incidents.reduce((acc, c) => { acc[c.priority] = (acc[c.priority] || 0) + 1; @@ -74,13 +95,13 @@ export default function CasesPanel({

Active Cases

{/* Case List */} -
+
{loading ? (

Loading incidents...

- ) : incidents.length === 0 ? ( + ) : sortedIncidents.length === 0 ? (

No incidents reported.

) : ( - incidents.map((c) => ( + sortedIncidents.map((c) => ( { const [activeView, setActiveView] = useState("Ambulances"); const [focusedUnit, setFocusedUnit] = useState(null); + const [focusedUnitId, setFocusedUnitId] = useState(null); + const [focusedIncidentId, setFocusedIncidentId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState( null ); @@ -47,6 +49,7 @@ const Dashboard = () => { loading: dispatchLoading, findBest, accept, + decline, declineAndReassign, } = useDispatchSuggestion(); @@ -126,6 +129,16 @@ const Dashboard = () => { setActiveView(view); }; + const handleIncidentClick = (incidentId: string) => { + setActiveView("Cases"); + setFocusedIncidentId(incidentId); + }; + + const handleAmbulanceClick = (unitId: string) => { + setActiveView("Ambulances"); + setFocusedUnitId(unitId); + }; + const renderLeftPanel = () => { if (activeView === "Ambulances") { return ( @@ -134,6 +147,7 @@ const Dashboard = () => { handleViewChange={handleViewChange} units={units} onUnitClick={(unit) => setFocusedUnit(unit)} + focusedUnitId={focusedUnitId} /> ); } else if (activeView === "Cases") { @@ -146,6 +160,7 @@ const Dashboard = () => { onDispatch={findBest} dispatchLoading={dispatchLoading} dispatchInfoMap={dispatchInfoMap} + focusedIncidentId={focusedIncidentId} /> ); } else { @@ -174,7 +189,12 @@ const Dashboard = () => { hospitals={hospitals} dispatchSuggestion={suggestion} onAcceptSuggestion={accept} + onDismissSuggestion={decline} onDeclineSuggestion={declineAndReassign} + onIncidentClick={handleIncidentClick} + onDispatch={findBest} + dispatchLoading={dispatchLoading} + onAmbulanceClick={handleAmbulanceClick} />
diff --git a/frontend/src/components/MapPanel.tsx b/frontend/src/components/MapPanel.tsx index 8b7b36e..55e2fe2 100644 --- a/frontend/src/components/MapPanel.tsx +++ b/frontend/src/components/MapPanel.tsx @@ -38,7 +38,12 @@ interface MapPanelProps { hospitals?: Hospital[]; dispatchSuggestion?: DispatchSuggestion | null; onAcceptSuggestion?: () => void; + onDismissSuggestion?: () => void; onDeclineSuggestion?: () => void; + onIncidentClick?: (incidentId: string) => void; + onDispatch?: (incidentId: string) => void; + dispatchLoading?: boolean; + onAmbulanceClick?: (unitId: string) => void; } const PRIORITY_COLORS: Record = { @@ -102,7 +107,12 @@ export default function MapPanel({ hospitals = [], dispatchSuggestion, onAcceptSuggestion, + onDismissSuggestion, onDeclineSuggestion, + onIncidentClick, + onDispatch, + dispatchLoading, + onAmbulanceClick, }: MapPanelProps) { const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY; const { isLoaded } = useLoadScript({ googleMapsApiKey: apiKey! }); @@ -131,6 +141,7 @@ export default function MapPanel({ mapRef.current = map; }} mapContainerStyle={{ width: "100%", height: "100%" }} + options={{ clickableIcons: false }} > {units.map((unit) => { const position = parseCoords(unit.coords); @@ -151,6 +162,8 @@ export default function MapPanel({ >
onAmbulanceClick?.(unit.id)} + style={{ cursor: "pointer" }} >
@@ -180,7 +193,7 @@ export default function MapPanel({ options={{ pixelOffset: new google.maps.Size(0, -30), }} - onCloseClick={onDeclineSuggestion} + onCloseClick={onDismissSuggestion} >
setHoveredIncidentId(incident.id)} onMouseLeave={() => setHoveredIncidentId(null)} - style={{ position: "relative" }} + onClick={() => onIncidentClick?.(incident.id)} + style={{ position: "relative", cursor: "pointer" }} > +
{incident.location}
-
+
Reported at {formatTime(incident.reported_at)}
+ {incident.status === "open" && onDispatch && ( + + )} +
)}