Signal/signal-ui/src/App.jsx
Kisa d4e5187350 Add OrganizationSwitcher to header
Lets users switch between orgs (e.g. STTIL Solutions, Gaboro DME)
directly from the top bar without going into account settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 13:15:37 -04:00

159 lines
No EOL
5.4 KiB
JavaScript

import { useState, useCallback, useRef } from "react";
import { SignIn, UserButton, OrganizationSwitcher, useAuth } from "@clerk/react";
import { ThemeProvider } from "./ThemeProvider";
import Sidebar from "./components/Sidebar";
import StatCard from "./components/StatCard";
import WorklistTable from "./components/WorklistTable";
import CSVImport from "./components/CSVImport";
import CSVExport from "./components/CSVExport";
import ThemeToggle from "./components/ThemeToggle";
import Toast from "./components/Toast";
import { showToast } from "./lib/toast";
import { uploadToBackend, apiRecordToLocal } from "./lib/api";
import { parseCSV, processBatch } from "./lib/coverage";
function AppInner() {
const { getToken } = useAuth();
const [records, setRecords] = useState([]);
const [batchId, setBatchId] = useState(null);
const [activeFilter, setActiveFilter] = useState("all");
const [importLabel, setImportLabel] = useState("No data imported");
const csvImportRef = useRef(null);
const ooc = records.filter((r) => r.flag === "OUT_OF_COVERAGE").length;
const visitDue = records.filter((r) => r.flag === "VISIT_DUE").length;
const refill = records.filter((r) => r.flag === "REFILL_WINDOW").length;
const okCount = records.filter((r) => r.flag === "OK").length;
const urgent = ooc + visitDue;
const handleResults = useCallback(async (file) => {
const label = new Date().toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
const token = await getToken().catch(() => null);
const data = await uploadToBackend(file, token);
if (data) {
const results = data.records.map(apiRecordToLocal);
setRecords(results);
setBatchId(data.batch_id || null);
setImportLabel(`${file.name} · ${label} · via Signal API`);
let msg = `Loaded ${data.total} patient${data.total !== 1 ? "s" : ""} from ${file.name}`;
if (data.skipped) msg += ` · ${data.skipped} skipped`;
showToast(msg);
return;
}
// Backend unreachable — process locally
setBatchId(null);
const reader = new FileReader();
reader.onload = (e) => {
const rows = parseCSV(e.target.result);
const { results, skipped } = processBatch(rows);
setRecords(results);
setImportLabel(`${file.name} · ${label} · local processing`);
let msg = `Loaded ${results.length} patient${results.length !== 1 ? "s" : ""} from ${file.name}`;
if (skipped.length) msg += ` · ${skipped.length} skipped`;
showToast(msg);
};
reader.readAsText(file);
}, [getToken]);
return (
<div className="flex w-full min-h-screen bg-[var(--bg-page)] text-[var(--text-primary)] transition-colors">
{/* Sidebar */}
<Sidebar
oocCount={ooc}
visitDueCount={visitDue}
refillCount={refill}
activeFilter={activeFilter}
onFilterChange={setActiveFilter}
onImportClick={() => csvImportRef.current?.trigger()}
/>
{/* Hidden CSV import trigger */}
<CSVImport
ref={csvImportRef}
onResults={handleResults}
/>
{/* Main */}
<main className="ml-[240px] flex-1 flex flex-col">
{/* Topbar */}
<header className="bg-[var(--bg-card)] border-b border-[var(--border-color)] px-7 h-[60px] flex items-center justify-between sticky top-0 z-10 transition-colors">
<div className="flex items-center gap-4">
<h1 className="font-heading font-bold text-lg text-[var(--text-heading)]">
Outreach Worklist
</h1>
<span className="font-mono text-[11px] text-[var(--text-muted)] px-2 py-[3px] bg-[var(--bg-elevated)] rounded">
{importLabel}
</span>
</div>
<div className="flex items-center gap-[10px]">
<ThemeToggle />
<CSVExport records={records} batchId={batchId} />
<OrganizationSwitcher hidePersonal />
<UserButton />
</div>
</header>
{/* Content */}
<div className="p-7">
{/* Stats row */}
<div className="grid grid-cols-3 gap-4 mb-6">
<StatCard
priority
label="⚡ Prescriber Action"
value={urgent}
sub={`${ooc} out of coverage · ${visitDue} visit${visitDue !== 1 ? "s" : ""} due — prescriber contact required`}
/>
<StatCard
label="⚠ Resupply Ready"
value={refill}
sub="patients within resupply window — initiate now"
/>
<StatCard
label="✓ Active"
value={okCount}
sub="patients with supply on track — no action needed"
/>
</div>
{/* Worklist */}
<WorklistTable
records={records}
activeFilter={activeFilter}
onFilterChange={setActiveFilter}
/>
</div>
</main>
<Toast />
</div>
);
}
export default function App() {
const { isSignedIn, isLoaded } = useAuth();
if (!isLoaded) {
return <div className="flex items-center justify-center min-h-screen bg-[#F0EAE1]" />;
}
if (!isSignedIn) {
return (
<div className="flex items-center justify-center min-h-screen bg-[#F0EAE1]">
<SignIn routing="hash" />
</div>
);
}
return (
<ThemeProvider>
<AppInner />
</ThemeProvider>
);
}