MEK Charge Reconciliation: Waystar + Aquilaehr download, per-location reconciliation, web app
- mek_recon.py: Playwright download for both sites in parallel (headless on server) - consolidate.py: Patient Name + DOS match, per-location and All Locations Excel - app.py: sign-in (server-side password), Run button, per-file and ZIP downloads - Dockerfile for Dokploy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
fd480fa874
10 files changed
+1335
No files matched your search
@@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MEK Charge Reconciliation</title>
|
||||
<style>
|
||||
/* same night palette as the sign-in page */
|
||||
:root {
|
||||
--night: #040b26;
|
||||
--panel: #0c1638;
|
||||
--panel-line: #26335e;
|
||||
--field: #050c28;
|
||||
--pill-line: #c9d5f2;
|
||||
--ink: #eef2ff;
|
||||
--ink-2: #a9b6d8;
|
||||
--owl: #ec7d33;
|
||||
--ok: #7fe0a8;
|
||||
--error: #ff9a9a;
|
||||
--ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
color-scheme: dark;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--night); color: var(--ink); font: 15px/1.5 var(--ui); }
|
||||
main { width: min(860px, 100%); margin: 0 auto; padding: 32px 16px 48px; }
|
||||
header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
|
||||
h1 { margin: 0; font-size: clamp(20px, 5vw, 24px); font-weight: 650; letter-spacing: -.01em; }
|
||||
h2 { margin: 0 0 12px; font-size: 17px; font-weight: 650; }
|
||||
.card { background: var(--panel); border: 1px solid var(--panel-line); border-radius: 18px; padding: 22px 22px 20px; margin-bottom: 16px; }
|
||||
.muted { color: var(--ink-2); }
|
||||
.pill {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
height: 44px; padding: 0 22px; background: #0a1435; color: #fff; text-decoration: none;
|
||||
border: 1.5px solid var(--pill-line); border-radius: 999px; font: 600 15px var(--ui); cursor: pointer;
|
||||
}
|
||||
.pill:hover { border-color: var(--owl); background: #101d45; }
|
||||
.pill:focus-visible, a:focus-visible { outline: 2px solid var(--owl); outline-offset: 3px; }
|
||||
.pill.primary { border-color: var(--owl); }
|
||||
.pill[disabled] { opacity: .5; cursor: not-allowed; }
|
||||
.pill.small { height: 34px; padding: 0 14px; font-size: 13px; }
|
||||
.row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
|
||||
.status { font-weight: 600; }
|
||||
.status.running { color: var(--owl); }
|
||||
.status.ok { color: var(--ok); }
|
||||
.status.fail { color: var(--error); }
|
||||
pre {
|
||||
margin: 14px 0 0; max-height: 280px; overflow: auto; padding: 12px 14px;
|
||||
background: var(--field); border: 1px solid var(--panel-line); border-radius: 12px;
|
||||
font: 12.5px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
pre:empty { display: none; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
td { padding: 9px 4px; border-top: 1px solid var(--panel-line); vertical-align: middle; }
|
||||
td:last-child { text-align: right; white-space: nowrap; }
|
||||
.file { font-weight: 600; }
|
||||
.file small { display: block; font-weight: 400; color: var(--ink-2); }
|
||||
.empty { color: var(--ink-2); margin: 0; }
|
||||
form.inline { margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>MEK Charge Reconciliation</h1>
|
||||
<form class="inline" method="post" action="/logout"><button class="pill small" type="submit">Sign out</button></form>
|
||||
</header>
|
||||
|
||||
<section class="card" aria-labelledby="run-h">
|
||||
<h2 id="run-h">Run</h2>
|
||||
<p class="muted" style="margin:0 0 14px">Downloads Waystar claims and Aquilaehr encounters for every location at the same time,
|
||||
then builds one reconciliation report per location (Patient Name + Date of Service).</p>
|
||||
<div class="row">
|
||||
<button class="pill primary" id="run" type="button">Run reconciliation</button>
|
||||
<span class="status" id="status" aria-live="polite"></span>
|
||||
</div>
|
||||
<pre id="log" aria-label="Run log"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="dl-h">
|
||||
<div class="row" style="justify-content:space-between;margin-bottom:6px">
|
||||
<h2 id="dl-h" style="margin:0">Reports</h2>
|
||||
<a class="pill small" id="zip" href="/download-all">Download all (ZIP)</a>
|
||||
</div>
|
||||
<table><tbody id="files"></tbody></table>
|
||||
<p class="empty" id="no-files" hidden>No reports yet. Run the reconciliation first.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const runBtn = document.getElementById("run");
|
||||
const statusEl = document.getElementById("status");
|
||||
const logEl = document.getElementById("log");
|
||||
const filesEl = document.getElementById("files");
|
||||
const noFiles = document.getElementById("no-files");
|
||||
const zip = document.getElementById("zip");
|
||||
let timer;
|
||||
|
||||
const fmtTime = s => s ? new Date(s * 1000).toLocaleString() : "";
|
||||
|
||||
function render(st) {
|
||||
runBtn.disabled = st.running;
|
||||
statusEl.className = "status " + (st.running ? "running" : st.code === 0 ? "ok" : st.code == null ? "" : "fail");
|
||||
statusEl.textContent = st.running ? `Running… started ${fmtTime(st.started)}`
|
||||
: st.code === 0 ? `Finished ${fmtTime(st.finished)}`
|
||||
: st.code == null ? "" : `Failed (exit ${st.code}) ${fmtTime(st.finished)}. See the log.`;
|
||||
const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8;
|
||||
logEl.textContent = st.log;
|
||||
if (atBottom) logEl.scrollTop = logEl.scrollHeight;
|
||||
|
||||
filesEl.innerHTML = "";
|
||||
for (const f of st.files) {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.className = "file";
|
||||
name.textContent = f.name.replace(/\.xlsx$/, "");
|
||||
const meta = document.createElement("small");
|
||||
meta.textContent = `Updated ${fmtTime(f.mtime)} · ${(f.size / 1024).toFixed(0)} KB`;
|
||||
name.append(meta);
|
||||
const act = document.createElement("td");
|
||||
const a = document.createElement("a");
|
||||
a.className = "pill small";
|
||||
a.href = "/download/" + encodeURIComponent(f.name);
|
||||
a.textContent = "Download";
|
||||
act.append(a);
|
||||
tr.append(name, act);
|
||||
filesEl.append(tr);
|
||||
}
|
||||
noFiles.hidden = st.files.length > 0;
|
||||
zip.hidden = st.files.length === 0;
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(refresh, st.running ? 2000 : 15000);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const res = await fetch("/status");
|
||||
if (res.status === 401) return location.href = "/";
|
||||
render(await res.json());
|
||||
}
|
||||
|
||||
runBtn.addEventListener("click", async () => {
|
||||
runBtn.disabled = true;
|
||||
const res = await fetch("/run", { method: "POST" });
|
||||
if (res.status === 409) statusEl.textContent = "A run is already in progress.";
|
||||
refresh();
|
||||
});
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user