Compare commits
No commits in common. "3f2c479f86157e6d6483357f4cc7fdbe1c642321" and "56f3737e0cddcbeb61821dc252c57969acb96853" have entirely different histories.
3f2c479f86
...
56f3737e0c
13 changed files with 42 additions and 920 deletions
|
|
@ -4,7 +4,7 @@ v:1
|
|||
|
||||
ACTIVE: 2026-07-07 [CC]. READINESS-MODEL BACKEND COMPLETE: P1-P5 all SHIPPED + LIVE-VERIFIED (a3af499, d24340c, bd92126, a53728e, f28b5bf; Railway + Vercel deployed; doc_status migration applied to prod). 169 tests + Pi E2E 33/33 green. P6 (frontend nesting) is the ONLY remaining item — BLOCKED on Kisa approving docs/mockups/p6-worklist-mockup-2026-07-07.png (decisions A-D).
|
||||
|
||||
NEXT: (Kisa) signed-in click-through of the new worklist (upload a test-data CSV; probe: two-device override isolation, PTN CTA, Clear-to-Ship tab empty without plan_type, confirm-visit fan-out, per-line export opens in Excel); (Kisa) confirm OQ1 + the retained-legacy-stopgap deviation (both logged); (Kisa) attorney bundle send + Gaboro re-approach (her lanes); (CC next session) update pilot guide screenshots + demo narrative for the plan-type mapping step (flag to lead-content), CSV formula-injection sanitization backlog item. Queued behind Signal: UMSIS docs-only activation, '63% written off' Draft fix.
|
||||
NEXT: (Kisa) rule on P6 mockup v2 (docs/mockups/p6-worklist-mockup-v2-2026-07-07.png): Decision B-v2 (generalized purple data-gap family) + Decision E (data-gap patients stay Action Needed with a Data-gaps filter vs a new label; CC recommends the filter) -> then CC builds the frontend. A (nesting) + D (per-line export) LOCKED 2026-07-07; (Kisa) confirm OQ1 plan_type-ingestion decision recorded 2026-07-07 (shipped per umbrella plan; two-line revert if reversed); (Kisa) attorney bundle READY TO SEND (signal/pitch/legal/attorney-bundle-2026-07-07, 5 Word docs + sendable memo): confirm recipient (Clyde Mathes vs Bittinger/Nixon), entity name + formation state, arbitration-vs-courts posture, Partner Concepts carve-out keep/drop, then email; Gaboro re-approach per reactivation plan 00 (unchanged). Queued behind Signal: UMSIS docs-only activation, "63% written off" Draft fix.
|
||||
|
||||
PRIOR ACTIVE: 2026-06-28. Signal paused at clean checkpoint — Insight Engine in active BUILD phase for July 4 deadline. See insight-engine/context/current-state.md and insight-engine/handoff-clock.md for full state. Key progress today: x-sight guided experience designed (Compass) and approved by Kisa, lane-lock handoff clock created, frontend finish readiness assessed (15 ranked items). Claude is building the Compass landing + B1 deploy blocker + finish items. Signal READINESS MODEL still queued at the clean checkpoint from 2026-06-27.
|
||||
|
||||
|
|
@ -25,8 +25,6 @@ Insight Engine (July 4 deadline = tomorrow) is the active project. Key files:
|
|||
|
||||
## Decisions (last
|
||||
|
||||
- 2026-07-07 [Kisa/CC]: P6 SHIPPED (6181de7) — all five design decisions approved and built (patient nesting, purple data-gap family, extended legend, per-line export, honest-label data-gap filter). Readiness-model build P1-P6 COMPLETE in one day: 6 ship commits, every item independently audited before ship, adversarial review killed a false-green pre-ship, live-measured verification at each step. Known deviations logged: legacy DOCS_REQUIRED stopgap retained on the ?legacy=1 surface; OQ1 shipped per umbrella plan.
|
||||
|
||||
- 2026-07-07 [Kisa/CC]: P6 rulings round 1 — nesting layout APPROVED and per-line export APPROVED (both in KG). Failed-data principle set by Kisa: Signal never acts on rows whose required info was not mapped or provided, and never hides them — engine-dropped rows now surface in skipped_reasons (shipped + deployed same day). Plan Type Needed stays its own state (it selects the rules; not a chaseable document); all other file-level gaps get the purple family treatment pending Decision B-v2/E.
|
||||
|
||||
- 2026-07-07 [CC]: Readiness-model backend COMPLETE (P2-P5 shipped same day as P1): citation IDs live (R-registry), patient rollup + nested payload live, device-scoped overrides live (doc_status migration applied to prod; overrides now feed the verdict; the leak is dead, live-measured), plan-type enforcement live (zero greens without a mapped plan type, live-measured; Phase 2c deletion stays Kisa-gated). Every item independently audited SHIP before ship. P6 mockup rendered for Kisa (4 decisions annotated).
|
||||
|
|
@ -65,4 +63,4 @@ Insight Engine (July 4 deadline = tomorrow) is the active project. Key files:
|
|||
|
||||
State protocol: read = `git pull --rebase` then read this file (warn if Updated > 24h old); write = edit ACTIVE/NEXT, append one author-stamped decision, then commit + push. Full protocol in the `state-sync` skill. Detailed ship-status (what shipped / didn't / why) = docs/ship-status-ledger.md. TriLane = searchable archive, not live state.
|
||||
|
||||
## Updated: 2026-07-07 (CC: Signal readiness-model build COMPLETE P1-P6, deployed, live-verified; Kisa click-through + her attorney/Gaboro lanes are the open items.)
|
||||
## Updated: 2026-07-07 (CC: readiness-model backend P1-P5 complete, live-verified, deployed; P6 gated on Kisa mockup approval.)
|
||||
|
|
|
|||
|
|
@ -112,18 +112,3 @@ code-auditor SHIP verdict and measured live verification:
|
|||
Final state: 169 unit/API tests green, Pi E2E 33/33, live drives recorded
|
||||
above. P6 (frontend nesting) BLOCKED on Kisa's mockup approval:
|
||||
docs/mockups/p6-worklist-mockup-2026-07-07.png (decisions A-D annotated).
|
||||
|
||||
## Shipped: P6 patient-nested worklist (2026-07-07, Claude, commit 6181de7)
|
||||
|
||||
The readiness-model build is COMPLETE end to end. The worklist now renders
|
||||
one row per patient (rollup verdict), expanding into device lines with doc
|
||||
checklists and rule citations; green is earned only from the verdict. All
|
||||
five Kisa decisions honored (nesting, purple family, legend, per-line export,
|
||||
no-sixth-label data-gap filter). Not-processed strip + download; PTN CTA;
|
||||
device-scoped cycling returns the re-rolled patient via the P4 echo; Confirm
|
||||
Visit fans out. Legacy table at ?legacy=1 (stopgap intentionally retained
|
||||
there). Backend: /api/export lines[] (live-verified). Audit: SHIP; MEDIUM
|
||||
line-identity merge fix + 2 LOWs applied pre-ship. New bundle confirmed
|
||||
serving (marker strings measured). REMAINING: Kisa's signed-in click-through
|
||||
(6 probes listed in the audit); backlog: CSV formula-injection sanitization
|
||||
pass (pre-existing, both export paths).
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# BUSINESS ASSOCIATE AGREEMENT (Template v1)
|
||||
|
||||
> **DRAFT, for attorney review before use. Not legal advice.**
|
||||
> Prepared 2026-07-07. Generalized from a prior supplier-specific BAA into a reusable STTIL Solutions template. This is a FORWARD-LOOKING instrument. See "Status and posture" immediately below before reading the operative terms.
|
||||
> Prepared 2026-07-07. Generalized from the a prior supplier-specific BAA into a reusable STTIL Solutions template. This is a FORWARD-LOOKING instrument. See "Status and posture" immediately below before reading the operative terms.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Counsel should review this template as the instrument STTIL would execute at the
|
|||
**Effective Date:** __________ , 20__
|
||||
|
||||
**Between:**
|
||||
- **KJF Professional Services LLC d/b/a STTIL Solutions** ("Business Associate"), a Florida limited liability company, Arkisia (Kisa) Fenn, Managing Member
|
||||
- **KJF Professional Services LLC d/b/a STTIL Solutions** ("Business Associate"), a [STATE, confirm formation state with counsel] limited liability company, Kisa Fenn, Managing Member
|
||||
- **[COVERED ENTITY FULL LEGAL NAME]** ("Covered Entity"), [SIGNER NAME], [SIGNER TITLE]
|
||||
|
||||
Each a "Party" and together the "Parties."
|
||||
|
|
@ -177,7 +177,7 @@ Nothing in this Agreement confers any right, remedy, or claim upon any third par
|
|||
|
||||
### 5.4 Governing Law
|
||||
|
||||
This Agreement is governed by the laws of the State of Florida, without regard to conflict of law principles.
|
||||
This Agreement is governed by the laws of the State of [STATE, confirm formation state with counsel], without regard to conflict of law principles.
|
||||
|
||||
### 5.5 Entire Agreement
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ This Agreement, together with the LOI and the mutual NDA between the Parties, co
|
|||
|
||||
**KJF Professional Services LLC d/b/a STTIL Solutions (Business Associate)**
|
||||
By: ___________________________
|
||||
Name: Arkisia (Kisa) Fenn
|
||||
Name: Kisa Fenn
|
||||
Title: Managing Member
|
||||
Date: _________________________
|
||||
Email: [KISA EMAIL]
|
||||
|
|
@ -213,9 +213,9 @@ Each load-bearing factual claim is labeled GROUNDED (cited source) or GRAFTED (a
|
|||
|
||||
3. **Accounting of disclosures scope (GRAFTED as applied).** Confirm the scope of Section 2.8 as applied to a software service that processes de-identified or minimum-necessary data only.
|
||||
|
||||
4. **State-specific requirements (GRAFTED).** Formation state is Florida (founder-confirmed 2026-07-07). Confirm which state's health-data statutes (beyond HIPAA) apply given Florida formation and each Covered Entity's location. A prior supplier-specific version governed under Pennsylvania; this template now uses Florida. Confirm no additional state-law obligations attach.
|
||||
4. **State-specific requirements (GRAFTED).** Confirm which state's health-data statutes (beyond HIPAA) apply once formation state and Covered Entity location are known. A prior supplier-specific version governed under Pennsylvania; this template uses a placeholder. Confirm no additional state-law obligations attach.
|
||||
|
||||
5. **Governing law and entity alignment (GROUNDED, founder-confirmed).** Entity is "KJF Professional Services LLC d/b/a STTIL Solutions", a Florida limited liability company (founder-confirmed 2026-07-07; older drafts said Delaware and Pennsylvania). Confirm Section 5.4's Florida governing law aligns with the LOI and NDA as drafted.
|
||||
5. **Governing law and entity alignment (GROUNDED that documents disagree).** The v1 LOI and NDA said Delaware; the a prior supplier BAA said Pennsylvania; an executed letter of intent states Florida. Entity name is now "KJF Professional Services LLC d/b/a STTIL Solutions." Confirm the correct legal name and formation state and align Section 5.4 with the LOI and NDA.
|
||||
|
||||
6. **Forward-looking status (GROUNDED).** Real PHI is blocked/mock today (Signal current-state.md, 2026-07-07). Confirm this template is the correct instrument to execute at the point real PHI enters the system, and advise whether any interim data-handling addendum is needed for a pilot that stays inside the de-identified minimum-necessary field set before that point.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
> **DRAFT, for attorney review before use. Not legal advice.**
|
||||
> Cover memo prepared 2026-07-07. Bundles five items for a single review pass.
|
||||
|
||||
**To:** Clyde Mathes
|
||||
**To:** [ATTORNEY NAME, CONFIRM RECIPIENT before sending. Session records dated 2026-06-05 name Clyde Mathes as attorney of record; the Signal compliance checklist rows name Bittinger Law / Nixon Law Group. These conflict. Reconcile before this memo goes out.]
|
||||
**From:** Kisa Fenn, Founder and CEO, KJF Professional Services LLC d/b/a STTIL Solutions
|
||||
**Date:** [DATE]
|
||||
**Re:** Single-pass review of three template agreements plus two focused legal questions
|
||||
|
||||
---
|
||||
|
||||
Hello Clyde,
|
||||
Hello [ATTORNEY NAME],
|
||||
|
||||
I would like to engage you for one bundled review pass covering five items. I have grouped them deliberately so you can scope the whole thing at once rather than in pieces. An associate handling the bulk of this is completely fine with me. What I need first is a turnaround estimate and a fee estimate so I can plan around it.
|
||||
|
||||
|
|
@ -26,9 +26,9 @@ I have written my own focused questions under each item. Please treat these as a
|
|||
|
||||
### Overarching question (applies to all three templates)
|
||||
|
||||
**Q1. Confirm Florida governing law across every document.** Our entity is KJF Professional Services LLC d/b/a STTIL Solutions, a Florida limited liability company. I have confirmed the name and formation state; older drafts disagreed (Delaware, Pennsylvania), so all three templates now state Florida consistently. Please verify the registration and confirm Florida is the right governing-law and venue choice for us in each document.
|
||||
**Q1. Confirm our legal name, our state of formation, and align governing law across every document.** Our entity is KJF Professional Services LLC d/b/a STTIL Solutions. Our documents currently disagree on the state: two older templates say Delaware, an executed letter of intent says Florida, and an older business associate agreement says Pennsylvania. I have replaced the state with a placeholder everywhere so nothing is asserted incorrectly. Please tell me the correct legal name and formation state, and set governing law and dispute-resolution venue consistently across the LOI, NDA, and BAA.
|
||||
|
||||
**Q2. Standardize my signing title and name.** The LOI and NDA sign as "Founder and CEO" while the BAA signs as "Managing Member." Tell me which title is correct for an LLC signer and I will use it across all three. Also confirm the name format: my legal name is Arkisia Fenn (I go by Kisa), and I signed our executed letter of intent as Arkisia Fenn.
|
||||
**Q2. Standardize my signing title.** The LOI and NDA sign as "Founder and CEO" while the BAA signs as "Managing Member." Tell me which title is correct for an LLC signer and I will use it across all three.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ I have written my own focused questions under each item. Please treat these as a
|
|||
1. The BAA cross-reference in Section 5.3 turns on whether we are a Business Associate at all. That is the BAA question below; please keep the two consistent.
|
||||
2. Section 8.3 (we retain all rights to any modification or enhancement made during a pilot, regardless of who requested it) and Section 8.4 (a narrow carve-out for a partner's pre-documented pre-existing concepts). Confirm both are enforceable and that 8.4 does not erode 8.3.
|
||||
3. Section 12.3, a new mutual exclusion of consequential damages for a no-cost pilot. Confirm it is adequate and enforceable.
|
||||
4. Section 14, our dispute-resolution path (30-day good-faith discussion, then arbitration, with the American Arbitration Association (AAA) as fallback, and a court carve-out for injunctive relief). I have set arbitration as our default posture, and the NDA now mirrors it in Article 10.2; the structure follows an agreement we have already signed once. Confirm both clauses as drafted.
|
||||
4. Section 14, a dispute-resolution path (30-day good-faith discussion, then arbitration, with the American Arbitration Association (AAA) as fallback, and a court carve-out for injunctive relief). We added this after an out-of-state counterparty objected to exclusive Delaware court jurisdiction. Tell me whether to keep arbitration here and whether to match it in the NDA, which currently uses courts.
|
||||
5. Sections 6.1 and 13.3, confirming that a successful pilot does not obligate us to offer a commercial agreement. Confirm this closes any promissory-estoppel exposure.
|
||||
|
||||
### 2. Mutual NDA template (v3)
|
||||
|
|
@ -68,7 +68,7 @@ The memo is at `signal/docs/compliance/fda-cds-exemption-memo-DRAFT.md`. I am no
|
|||
|
||||
**Confidentiality note:** the partner named here is bound to us by a confidentiality clause that bars public mention of the relationship. I am sharing the name with you only because you are our counsel and I need the facts to get sound advice. Please keep it inside privilege.
|
||||
|
||||
We have an executed 180-day discovery letter of intent with Pathogens Artificial Intelligence Inc., a Delaware corporation doing business as LabABLE, a diagnostic-laboratory software platform whose team includes Dr. Mehul Patel (the LOI was signed by co-founder Daniel Rojas). During discovery we discussed a reciprocal integration concept, informally called "kit in kit": LabABLE could be made available as a component inside our Signal platform, and Signal could be made available as a component inside the LabABLE platform.
|
||||
We have an executed 180-day discovery letter of intent with Pathogens Artificial Intelligence Inc., which does business as LabABLE, a diagnostic-laboratory software platform run by Dr. Mehul Patel's team. During discovery we discussed a reciprocal integration concept, informally called "kit in kit": LabABLE could be made available as a component inside our Signal platform, and Signal could be made available as a component inside the LabABLE platform.
|
||||
|
||||
Both of our customer bases serve providers who bill federal healthcare programs (our suppliers bill Medicare for DMEPOS; laboratories bill Medicare Part B). My question:
|
||||
|
||||
|
|
@ -87,9 +87,9 @@ This must be resolved before we execute any further agreement with them, so plea
|
|||
|
||||
Thank you. I am happy to get on a call to walk through any of it.
|
||||
|
||||
Kisa (Arkisia) Fenn
|
||||
Kisa Fenn
|
||||
KJF Professional Services LLC d/b/a STTIL Solutions
|
||||
kisasttil@gmail.com / [PHONE]
|
||||
[KISA EMAIL] / [PHONE]
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -97,8 +97,8 @@ kisasttil@gmail.com / [PHONE]
|
|||
|
||||
Load-bearing facts in this memo, labeled GROUNDED (cited source) or GRAFTED (assumption to confirm):
|
||||
|
||||
- Recipient RESOLVED: Clyde Mathes, confirmed by Kisa 2026-07-07 (the Bittinger/Nixon names in the compliance checklist were candidate firms).
|
||||
- labABLE LOI VERIFIED against the executed PDF 2026-07-07 (`signal/pitch/legal/labABLE-LOI-v3-EXECUTED-2026-06-12.pdf`): signed 6/12/2026 by Daniel Rojas (Co-Founder, Pathogens Artificial Intelligence Inc.) and Arkisia Fenn (Founder); binding sections 3 through 11; 180-day term from Effective Date; governed by Delaware law on LabABLE's paper; Section 7 publicity clause bars public mention without written consent. The kit-in-kit concept does NOT appear in the executed text; it is a discussed concept only, which is exactly how this memo frames it.
|
||||
- Recipient is unresolved. GROUNDED that 2026-06-05 records name Clyde Mathes and the Signal compliance checklist names Bittinger/Nixon; the two conflict (Signal CLAUDE.md; FDA memo routing note). Confirm before sending. GRAFTED which is correct.
|
||||
- labABLE LOI executed 2026-06-12, 180-day discovery term. GROUNDED (KG Signal; executed PDF `signal/pitch/legal/labABLE-LOI-v3-EXECUTED-2026-06-12.pdf`). Verify signer names and clause numbers against the executed PDF before any citation; the "kit-in-kit" description here is from the 2026-06-06 markup, not the final executed text.
|
||||
- labABLE legal entity is Pathogens Artificial Intelligence Inc. d/b/a LabABLE. GROUNDED (labABLE LOI markup 2026-06-06).
|
||||
- AWS BAA signed and active 2026-05-27, RDS and S3 provisioned and parked. GROUNDED (KG Signal; Signal current-state.md).
|
||||
- Real PHI blocked/mock today. GROUNDED (Signal current-state.md 2026-07-07).
|
||||
|
|
|
|||
|
|
@ -925,34 +925,9 @@ async def upload_csv(
|
|||
)
|
||||
|
||||
|
||||
class LineExportRow(BaseModel):
|
||||
"""One device coverage line for the per-line work-queue export
|
||||
(granularity locked by Kisa 2026-07-07)."""
|
||||
|
||||
patient_id: str
|
||||
device_display: str = ""
|
||||
payer: str = ""
|
||||
plan_type: Optional[str] = None
|
||||
rollup_status: Optional[str] = None
|
||||
line_status: Optional[str] = None
|
||||
timing: str = ""
|
||||
order_numbers: str = ""
|
||||
hcpcs_codes: str = ""
|
||||
total_quantity: int = 0
|
||||
shipment_date: str = ""
|
||||
swo: str = ""
|
||||
visit: str = ""
|
||||
pecos: str = ""
|
||||
pa: str = ""
|
||||
diagnosis: str = ""
|
||||
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
records: list[RecordOut] = []
|
||||
records: list[RecordOut]
|
||||
batch_id: str | None = None
|
||||
# Per-line export (P6): when present, the CSV is one row per device
|
||||
# coverage line instead of one row per legacy record.
|
||||
lines: Optional[list[LineExportRow]] = None
|
||||
|
||||
|
||||
@app.post("/api/export")
|
||||
|
|
@ -960,81 +935,10 @@ async def export_work_queue(
|
|||
body: ExportRequest,
|
||||
claims: dict = Depends(require_auth),
|
||||
):
|
||||
"""Generate a downloadable work-queue CSV from a list of scored records,
|
||||
or one row per device coverage line when `lines` is provided (P6)."""
|
||||
"""Generate a downloadable work-queue CSV from a list of scored records."""
|
||||
records = body.records
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
if body.lines is not None:
|
||||
writer.writerow(
|
||||
[
|
||||
"Patient ID",
|
||||
"Device",
|
||||
"Payer",
|
||||
"Plan Type",
|
||||
"Patient Status",
|
||||
"Line Status",
|
||||
"Timing",
|
||||
"Order Numbers",
|
||||
"HCPCS",
|
||||
"Quantity",
|
||||
"Last Shipment",
|
||||
"SWO",
|
||||
"Visit",
|
||||
"PECOS",
|
||||
"PA",
|
||||
"Diagnosis",
|
||||
]
|
||||
)
|
||||
for l in body.lines:
|
||||
writer.writerow(
|
||||
[
|
||||
l.patient_id,
|
||||
l.device_display,
|
||||
l.payer,
|
||||
l.plan_type or "Plan type needed",
|
||||
l.rollup_status or "",
|
||||
l.line_status or "",
|
||||
l.timing,
|
||||
l.order_numbers,
|
||||
l.hcpcs_codes,
|
||||
l.total_quantity,
|
||||
l.shipment_date,
|
||||
l.swo,
|
||||
l.visit,
|
||||
l.pecos,
|
||||
l.pa,
|
||||
l.diagnosis,
|
||||
]
|
||||
)
|
||||
output.seek(0)
|
||||
today = date.today().isoformat()
|
||||
export_filename = f"signal-work-queue-{today}.csv"
|
||||
from core.supabase_client import get_client
|
||||
|
||||
log_event(
|
||||
action=AuditAction.WORKLIST_EXPORT,
|
||||
resource_id=export_filename,
|
||||
user_id=claims.get("sub", "demo_user"),
|
||||
outcome="success",
|
||||
ip_address="0.0.0.0",
|
||||
detail=f"{len(body.lines)} coverage lines exported",
|
||||
db_conn=get_client(),
|
||||
)
|
||||
persist_export(
|
||||
batch_id=body.batch_id,
|
||||
filename=export_filename,
|
||||
row_count=len(body.lines),
|
||||
)
|
||||
return StreamingResponse(
|
||||
io.BytesIO(output.getvalue().encode("utf-8")),
|
||||
media_type="text/csv",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={export_filename}"
|
||||
},
|
||||
)
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
"Order Number",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { ThemeProvider } from "./ThemeProvider";
|
|||
import Sidebar from "./components/Sidebar";
|
||||
import StatCard from "./components/StatCard";
|
||||
import WorklistTable from "./components/WorklistTable";
|
||||
import PatientWorklist from "./components/PatientWorklist";
|
||||
import CSVImport from "./components/CSVImport";
|
||||
import CSVExport from "./components/CSVExport";
|
||||
import ThemeToggle from "./components/ThemeToggle";
|
||||
|
|
@ -18,15 +17,10 @@ import { parseCSV, processBatch } from "./lib/coverage";
|
|||
function AppInner() {
|
||||
const { getToken } = useAuth();
|
||||
const [records, setRecords] = useState([]);
|
||||
const [uploadData, setUploadData] = useState(null); // full API payload (P6)
|
||||
const [batchId, setBatchId] = useState(null);
|
||||
const [activeFilter, setActiveFilter] = useState("all");
|
||||
const [importLabel, setImportLabel] = useState("No data imported");
|
||||
const csvImportRef = useRef(null);
|
||||
// The nested patient view is the door (P6, Kisa-approved); ?legacy=1 keeps
|
||||
// the old row-keyed table reachable during the transition (Phase 2b).
|
||||
const useLegacyView =
|
||||
new URLSearchParams(window.location.search).get("legacy") === "1";
|
||||
|
||||
const supplyLapsed = records.filter((r) => r.flag === "SUPPLY_LAPSED").length;
|
||||
const visitRequired = records.filter((r) => r.flag === "VISIT_REQUIRED").length;
|
||||
|
|
@ -56,7 +50,6 @@ function AppInner() {
|
|||
if (data) {
|
||||
const results = data.records.map(apiRecordToLocal);
|
||||
setRecords(results);
|
||||
setUploadData(data);
|
||||
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}`;
|
||||
|
|
@ -72,10 +65,8 @@ function AppInner() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Backend unreachable — process locally (legacy table renders; the
|
||||
// nested view needs the API's patients payload)
|
||||
// Backend unreachable — process locally
|
||||
setBatchId(null);
|
||||
setUploadData(null);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const rows = parseCSV(e.target.result);
|
||||
|
|
@ -123,11 +114,7 @@ function AppInner() {
|
|||
</div>
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<ThemeToggle />
|
||||
<CSVExport
|
||||
records={records}
|
||||
patients={!useLegacyView ? uploadData?.patients : null}
|
||||
batchId={batchId}
|
||||
/>
|
||||
<CSVExport records={records} batchId={batchId} />
|
||||
<OrganizationSwitcher hidePersonal />
|
||||
<UserButton />
|
||||
</div>
|
||||
|
|
@ -161,14 +148,7 @@ function AppInner() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Worklist — nested patient view (P6) unless ?legacy=1 or the
|
||||
payload lacks patients (local fallback processing) */}
|
||||
{!useLegacyView && uploadData?.patients?.length ? (
|
||||
<PatientWorklist
|
||||
data={uploadData}
|
||||
onImportClick={() => csvImportRef.current?.trigger()}
|
||||
/>
|
||||
) : (
|
||||
{/* Worklist */}
|
||||
<WorklistTable
|
||||
records={records}
|
||||
activeFilter={activeFilter}
|
||||
|
|
@ -179,7 +159,6 @@ function AppInner() {
|
|||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -86,52 +86,6 @@ const FLAG_CONFIG = {
|
|||
},
|
||||
};
|
||||
|
||||
// P6 verdict badges — the FIVE sanctioned line/rollup statuses, verbatim.
|
||||
// Colors per the Kisa-approved mockup (2026-07-07): stoplight for the four
|
||||
// locked labels; purple for Plan Type Needed (configuration, not urgency).
|
||||
const VERDICT_CONFIG = {
|
||||
"At Risk": {
|
||||
bg: "bg-[rgba(204,34,34,0.10)]", text: "text-[#CC2222]",
|
||||
border: "border-[#CC2222]", weight: "font-bold", icon: "⚠",
|
||||
},
|
||||
"Action Needed": {
|
||||
bg: "bg-[rgba(203,107,32,0.12)]", text: "text-[#CB6B20]",
|
||||
border: "border-[#CB6B20]", weight: "font-semibold", icon: "→",
|
||||
},
|
||||
"On Track": {
|
||||
bg: "bg-[rgba(46,163,163,0.10)]", text: "text-[#147A7A]",
|
||||
border: "border-[#2EA3A3]", weight: "font-normal", icon: "●",
|
||||
},
|
||||
"Clear to Ship": {
|
||||
bg: "bg-[rgba(34,197,94,0.12)]", text: "text-[#166534]",
|
||||
border: "border-[#22C55E]", weight: "font-bold", icon: "✓",
|
||||
},
|
||||
"Plan Type Needed": {
|
||||
bg: "bg-[rgba(122,94,160,0.10)]", text: "text-[#6B4F9A]",
|
||||
border: "border-[#9B8EC4]", weight: "font-semibold", icon: "◆",
|
||||
},
|
||||
};
|
||||
|
||||
export function VerdictBadge({ status }) {
|
||||
if (!status) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-[3px] px-[8px] py-[3px] rounded-full text-[11px] border bg-[rgba(120,120,120,0.07)] text-[var(--text-warm)] border-[var(--border-color)] font-normal whitespace-nowrap">
|
||||
Informational
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const cfg = VERDICT_CONFIG[status];
|
||||
if (!cfg) return null;
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-[3px] px-[8px] py-[3px] rounded-full text-[11px] border ${cfg.bg} ${cfg.text} ${cfg.border} ${cfg.weight} whitespace-nowrap`}
|
||||
>
|
||||
<span className="text-[10px]">{cfg.icon}</span>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Badge({ flag }) {
|
||||
const cfg = FLAG_CONFIG[flag] || {
|
||||
label: flag,
|
||||
|
|
|
|||
|
|
@ -1,40 +1,7 @@
|
|||
import { useAuth } from "@clerk/react";
|
||||
import { exportFromBackend, exportLinesFromBackend } from "../lib/api";
|
||||
import { exportFromBackend } from "../lib/api";
|
||||
import { getFlagLabel, getFlagAction, getDeviceDisplay } from "../lib/coverage";
|
||||
|
||||
// One export row per device coverage line (P6 granularity, locked 2026-07-07).
|
||||
function patientsToLineRows(patients) {
|
||||
const rows = [];
|
||||
for (const p of patients) {
|
||||
for (const line of p.lines || []) {
|
||||
if (!line.gradeable) continue; // non-CGM lines carry no readiness actions
|
||||
const by = {};
|
||||
(line.doc_items || []).forEach((i) => {
|
||||
by[i.doc_type] = i.value || (i.required_state === "NOT_REQUIRED" ? "Not required" : "Not on file");
|
||||
});
|
||||
rows.push({
|
||||
patient_id: p.patient_id,
|
||||
device_display: line.device_display,
|
||||
payer: line.payer || "",
|
||||
plan_type: line.plan_type,
|
||||
rollup_status: p.rollup_status,
|
||||
line_status: line.line_status,
|
||||
timing: line.timing_flag || "",
|
||||
order_numbers: (line.order_numbers || []).join(", "),
|
||||
hcpcs_codes: (line.hcpcs_codes || []).join(", "),
|
||||
total_quantity: line.total_quantity || 0,
|
||||
shipment_date: line.shipment_date || "",
|
||||
swo: by.swo || "",
|
||||
visit: by.visit || "",
|
||||
pecos: by.pecos || "",
|
||||
pa: by.pa || "",
|
||||
diagnosis: by.diagnosis || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
|
|
@ -46,30 +13,15 @@ function downloadBlob(blob, filename) {
|
|||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export default function CSVExport({ records, patients = null, batchId }) {
|
||||
export default function CSVExport({ records, batchId }) {
|
||||
const { getToken } = useAuth();
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const filename = `signal-work-queue-${today}.csv`;
|
||||
|
||||
const exportCSV = async () => {
|
||||
if ((!records || records.length === 0) && !(patients && patients.length)) return;
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
// Nested view: one row per device coverage line, exported through the
|
||||
// backend so the WORM audit trail records it.
|
||||
if (patients && patients.length) {
|
||||
const token = await getToken().catch(() => null);
|
||||
try {
|
||||
const blob = await exportLinesFromBackend(patientsToLineRows(patients), batchId, token);
|
||||
if (blob) {
|
||||
downloadBlob(blob, filename);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// fall through to the legacy paths below
|
||||
}
|
||||
}
|
||||
|
||||
if (batchId && records && records.length) {
|
||||
if (batchId) {
|
||||
const token = await getToken().catch(() => null);
|
||||
const blob = await exportFromBackend(records, batchId, token);
|
||||
if (blob) {
|
||||
|
|
|
|||
|
|
@ -74,11 +74,6 @@ export default function ConfirmVisitModal({ record, onClose, onConfirmed, onSave
|
|||
csv_transfer_from: record.csv_transfer_from || null,
|
||||
order_number: record.order_number || null,
|
||||
hcpcs: record.hcpcs || null,
|
||||
// P6 nested view: plan type + the patient's echoed lines so the
|
||||
// response carries the re-rolled patient (visit fans out to all
|
||||
// lines). Legacy records lack these — behavior unchanged.
|
||||
...(record.plan_type ? { plan_type: record.plan_type } : {}),
|
||||
...(record._echo_lines ? { lines: record._echo_lines } : {}),
|
||||
}, token);
|
||||
onConfirmed(record.patient_id, updated);
|
||||
onSaveSuccess?.({ patient_id: record.patient_id });
|
||||
|
|
|
|||
|
|
@ -1,568 +0,0 @@
|
|||
import { useState, useEffect, Fragment } from "react";
|
||||
import { useAuth } from "@clerk/react";
|
||||
import { VerdictBadge } from "./Badge";
|
||||
import StatusLegend from "./StatusLegend";
|
||||
import ConfirmVisitModal from "./ConfirmVisitModal";
|
||||
import { updateDocStatus } from "../lib/api";
|
||||
|
||||
// P6 patient-nested worklist (Kisa-approved mockup, 2026-07-07).
|
||||
// One row per patient carrying the rollup verdict; expands into device
|
||||
// coverage lines with their doc checklists and rule citations. Green is
|
||||
// earned only from the readiness verdict. Tabs count PATIENTS.
|
||||
// The five status labels are locked; "Data gap" is a filter + chip, never
|
||||
// a sixth label (Kisa Decision E).
|
||||
|
||||
const DOC_LABELS = {
|
||||
swo: "SWO",
|
||||
visit: "Qualifying Visit",
|
||||
pecos: "PECOS Enrollment",
|
||||
pa: "Prior Authorization",
|
||||
diagnosis: "Diagnosis on File",
|
||||
};
|
||||
|
||||
// doc_type -> the canonical CSV field whose mapping decides whether an
|
||||
// absent item is a FILE gap (column never mapped) or a PATIENT gap.
|
||||
const DOC_COLUMN = {
|
||||
swo: "csv_swo_status",
|
||||
visit: "csv_visit_date",
|
||||
pecos: "csv_pecos_verified",
|
||||
pa: "csv_pa_status",
|
||||
diagnosis: "csv_diagnosis_on_file",
|
||||
};
|
||||
|
||||
const SWO_CYCLE = ["pending", "requested", "on_file"];
|
||||
const PA_CYCLE = ["not_required", "requested", "approved", "denied"];
|
||||
const DOC_STATUS_LABELS = {
|
||||
pending: "Pending", requested: "Requested", on_file: "On File",
|
||||
not_required: "Not Required", approved: "Approved", denied: "Denied",
|
||||
};
|
||||
|
||||
// Timing hints are descriptive phrases, never status labels (locked sets).
|
||||
const TIMING_HINTS = {
|
||||
SUPPLY_LAPSED: "Supply lapsed",
|
||||
VISIT_REQUIRED: "Visit overdue",
|
||||
RENEWAL_CRITICAL: "Visit renewal critical",
|
||||
RENEWAL_ELEVATED: "Visit renewal due soon",
|
||||
RENEWAL_SOON: "Visit renewal approaching",
|
||||
RESUPPLY_READY: "In resupply window",
|
||||
ACTIVE: "Timing on track",
|
||||
NO_RECENT_SHIPMENT: "No recent shipment",
|
||||
TRANSFER_PENDING: "Transfer pending",
|
||||
};
|
||||
|
||||
const TABS = [
|
||||
{ key: "all", label: "All Patients", color: "#5A7E7E" },
|
||||
{ key: "At Risk", label: "At Risk", color: "#CC2222" },
|
||||
{ key: "Action Needed", label: "Action Needed", color: "#CB6B20" },
|
||||
{ key: "On Track", label: "On Track", color: "#2EA3A3" },
|
||||
{ key: "Clear to Ship", label: "Clear to Ship", color: "#22C55E", check: true },
|
||||
{ key: "Plan Type Needed", label: "Plan Type Needed", color: "#9B8EC4" },
|
||||
];
|
||||
|
||||
function itemIsFileGap(item, mappedFields) {
|
||||
// REQUIRED only: NOT_EVALUATED items (plan type unknown) belong to the
|
||||
// Plan Type Needed treatment, never double-flagged as data gaps.
|
||||
return (
|
||||
item.input_state === "ABSENT" &&
|
||||
item.required_state === "REQUIRED" &&
|
||||
!(DOC_COLUMN[item.doc_type] in mappedFields)
|
||||
);
|
||||
}
|
||||
|
||||
function patientFileGaps(p, mappedFields) {
|
||||
const gaps = new Set();
|
||||
for (const line of p.lines || []) {
|
||||
for (const item of line.doc_items || []) {
|
||||
if (itemIsFileGap(item, mappedFields)) gaps.add(item.doc_type);
|
||||
}
|
||||
}
|
||||
return [...gaps];
|
||||
}
|
||||
|
||||
function lineToEcho(line) {
|
||||
const by = {};
|
||||
(line.doc_items || []).forEach((i) => { by[i.doc_type] = i.value; });
|
||||
return {
|
||||
device_type: line.device_type,
|
||||
gradeable: line.gradeable,
|
||||
plan_type: line.plan_type,
|
||||
shipment_date: line.shipment_date || null,
|
||||
csv_swo_status: by.swo || null,
|
||||
csv_visit_date: by.visit || null,
|
||||
csv_pecos_verified: by.pecos || null,
|
||||
csv_pa_status: by.pa || null,
|
||||
csv_diagnosis_on_file: by.diagnosis || null,
|
||||
};
|
||||
}
|
||||
|
||||
function lineToModalRecord(p, line) {
|
||||
const by = {};
|
||||
(line.doc_items || []).forEach((i) => { by[i.doc_type] = i.value; });
|
||||
return {
|
||||
patient_id: p.patient_id,
|
||||
last_shipment_date: line.shipment_date,
|
||||
payer: line.payer || "",
|
||||
device_type: line.device_type,
|
||||
quantity: line.total_quantity || 1,
|
||||
plan_type: line.plan_type,
|
||||
csv_visit_date: by.visit || null,
|
||||
csv_swo_status: by.swo || null,
|
||||
csv_pecos_verified: by.pecos || null,
|
||||
csv_pa_status: by.pa || null,
|
||||
csv_diagnosis_on_file: by.diagnosis || null,
|
||||
_echo_lines: (p.lines || []).map(lineToEcho),
|
||||
};
|
||||
}
|
||||
|
||||
// Merge a P4 recompute response ({lines, rollup_status}) into local state.
|
||||
// Line identity is (device_type, plan_type) — a patient can hold the same
|
||||
// device under two plan types (typed + unknown), so device alone is not a key.
|
||||
function mergeRecompute(p, recomputed) {
|
||||
if (!recomputed?.lines) return p;
|
||||
const lineKey = (l) => `${l.device_type}:${l.plan_type ?? ""}`;
|
||||
const byKey = {};
|
||||
recomputed.lines.forEach((l) => { byKey[lineKey(l)] = l; });
|
||||
return {
|
||||
...p,
|
||||
rollup_status: recomputed.rollup_status ?? p.rollup_status,
|
||||
lines: p.lines.map((line) => {
|
||||
const upd = byKey[lineKey(line)];
|
||||
return upd
|
||||
? { ...line, line_status: upd.line_status, doc_items: upd.doc_items ?? line.doc_items }
|
||||
: line;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default function PatientWorklist({ data, onImportClick }) {
|
||||
const { getToken } = useAuth();
|
||||
const [patients, setPatients] = useState(data.patients || []);
|
||||
const [tab, setTab] = useState("all");
|
||||
const [refine, setRefine] = useState(null); // null | "gaps" | "outreach"
|
||||
const [expanded, setExpanded] = useState(null); // patient_id
|
||||
const [confirming, setConfirming] = useState(null); // {record, patientId}
|
||||
const [showAllSkipped, setShowAllSkipped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPatients(data.patients || []);
|
||||
setExpanded(null);
|
||||
}, [data]);
|
||||
|
||||
const mappedFields = data.mapping_summary?.mapped || {};
|
||||
const skippedReasons = data.skipped_reasons || [];
|
||||
const skippedCount = data.skipped || 0;
|
||||
|
||||
const gradeable = patients.filter((p) => p.rollup_status);
|
||||
const gapPatients = patients.filter(
|
||||
(p) => patientFileGaps(p, mappedFields).length > 0
|
||||
);
|
||||
const outreachPatients = patients.filter(
|
||||
(p) =>
|
||||
["At Risk", "Action Needed"].includes(p.rollup_status) &&
|
||||
patientFileGaps(p, mappedFields).length === 0
|
||||
);
|
||||
|
||||
// Single derivation for all counts: always from the local patients state.
|
||||
const counts = { all: patients.length };
|
||||
TABS.slice(1).forEach((t) => {
|
||||
counts[t.key] = gradeable.filter((p) => p.rollup_status === t.key).length;
|
||||
});
|
||||
|
||||
let visible =
|
||||
tab === "all" ? patients : patients.filter((p) => p.rollup_status === tab);
|
||||
if (refine === "gaps") {
|
||||
visible = visible.filter((p) => patientFileGaps(p, mappedFields).length > 0);
|
||||
} else if (refine === "outreach") {
|
||||
visible = visible.filter((p) => outreachPatients.includes(p));
|
||||
}
|
||||
|
||||
function downloadSkipped() {
|
||||
const blob = new Blob(
|
||||
["Signal — rows not processed\n\n" + skippedReasons.join("\n") + "\n"],
|
||||
{ type: "text/plain;charset=utf-8;" }
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `signal-not-processed-${new Date().toISOString().slice(0, 10)}.txt`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function applyRecompute(patientId, recomputed) {
|
||||
setPatients((prev) =>
|
||||
prev.map((p) => (p.patient_id === patientId ? mergeRecompute(p, recomputed) : p))
|
||||
);
|
||||
}
|
||||
|
||||
const ungraded = patients.length - gradeable.length;
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[10px] shadow-[var(--shadow-card-md)] overflow-hidden transition-colors">
|
||||
{/* Header + patient tabs */}
|
||||
<div className="flex items-center justify-between px-[22px] py-4 border-b border-[var(--border-color)] bg-[var(--bg-elevated)] gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className="font-heading font-bold text-[15px] text-[var(--text-heading)]">
|
||||
Outreach Worklist
|
||||
</div>
|
||||
<div className="font-mono text-[11px] text-[var(--text-warm)] mt-[2px]">
|
||||
{patients.length} patients · {gradeable.length} gradeable
|
||||
{ungraded > 0 && ` · ${ungraded} informational only`}
|
||||
{skippedCount > 0 && ` · ${skippedCount} rows not processed`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-[6px] flex-wrap">
|
||||
{TABS.map((t) => {
|
||||
const count = counts[t.key] ?? 0;
|
||||
const pct =
|
||||
t.key !== "all" && gradeable.length > 0
|
||||
? Math.round((count / gradeable.length) * 100)
|
||||
: null;
|
||||
const isActive = tab === t.key;
|
||||
const indicatorColor = isActive ? "white" : t.color;
|
||||
return (
|
||||
<button
|
||||
key={t.key}
|
||||
onClick={() => setTab(t.key)}
|
||||
className={`px-3 py-[5px] rounded-full text-[11.5px] cursor-pointer font-body transition-all border inline-flex items-center gap-[5px] ${
|
||||
isActive
|
||||
? "bg-[var(--brand)] text-white border-[var(--brand)]"
|
||||
: "bg-transparent border-[var(--border-color)] text-[var(--text-secondary)] hover:border-[var(--brand)] hover:text-[var(--brand)]"
|
||||
}`}
|
||||
>
|
||||
{t.check ? (
|
||||
<span style={{ color: indicatorColor, fontSize: "12px", fontWeight: 800, lineHeight: 1 }}>✓</span>
|
||||
) : (
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", backgroundColor: indicatorColor, display: "inline-block", flexShrink: 0 }} />
|
||||
)}
|
||||
{t.label}
|
||||
<span style={{ opacity: 0.65, fontSize: "10.5px", fontWeight: 500 }}>
|
||||
{count}
|
||||
{pct !== null ? ` · ${pct}%` : ""}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refine pills — separates fix-the-file work from outreach work */}
|
||||
<div className="flex items-center gap-[10px] px-[22px] py-[8px] border-b border-[var(--border-subtle)] text-[11px] text-[var(--text-warm)]">
|
||||
Refine:
|
||||
<button
|
||||
onClick={() => setRefine(refine === "gaps" ? null : "gaps")}
|
||||
className={`inline-flex items-center gap-[5px] rounded-full px-[11px] py-[2px] text-[11px] font-semibold border cursor-pointer ${
|
||||
refine === "gaps"
|
||||
? "bg-[#6B4F9A] text-white border-[#6B4F9A]"
|
||||
: "bg-[rgba(122,94,160,0.10)] text-[#6B4F9A] border-[#9B8EC4]"
|
||||
}`}
|
||||
>
|
||||
◇ Data gaps <b>{gapPatients.length}</b>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRefine(refine === "outreach" ? null : "outreach")}
|
||||
className={`inline-flex items-center gap-[5px] rounded-full px-[11px] py-[2px] text-[11px] font-semibold border cursor-pointer ${
|
||||
refine === "outreach"
|
||||
? "bg-[var(--brand)] text-white border-[var(--brand)]"
|
||||
: "border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||
}`}
|
||||
>
|
||||
Outreach only <b>{outreachPatients.length}</b>
|
||||
</button>
|
||||
<span className="ml-auto italic text-[10.5px]">
|
||||
Data-gap patients keep their honest status; this separates fix-the-file work from call-the-prescriber work
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Not-processed strip — rows Signal never scores and never hides */}
|
||||
{skippedCount > 0 && (
|
||||
<div className="flex items-start gap-3 px-[22px] py-[10px] border-b border-[var(--border-subtle)] bg-[rgba(203,107,32,0.05)] text-[12px] text-[var(--text-secondary)]">
|
||||
<div className="flex-1">
|
||||
<b className="text-[var(--text-heading)]">
|
||||
{skippedCount} row{skippedCount !== 1 ? "s" : ""} not processed
|
||||
</b>{" "}
|
||||
— Signal never scores a row whose required information was not provided, and never hides one:
|
||||
<ul className="list-disc ml-5 mt-1">
|
||||
{(showAllSkipped ? skippedReasons : skippedReasons.slice(0, 3)).map((r, i) => (
|
||||
<li key={i}>{r}</li>
|
||||
))}
|
||||
</ul>
|
||||
{skippedReasons.length > 3 && !showAllSkipped && (
|
||||
<button className="text-[var(--brand)] font-semibold mt-1 cursor-pointer bg-transparent border-none p-0" onClick={() => setShowAllSkipped(true)}>
|
||||
Show all {skippedReasons.length}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={downloadSkipped} className="text-[var(--brand)] font-semibold whitespace-nowrap cursor-pointer bg-transparent border-none p-0">
|
||||
Download list ↓
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StatusLegend extended />
|
||||
|
||||
{/* Patient table */}
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
{["", "Patient", "Device(s)", "Payer", "Status", "Last Resupply", "Actions"].map((h, i) => (
|
||||
<th key={i} className="px-[18px] py-[10px] text-left text-[10.5px] font-semibold tracking-[0.06em] uppercase text-[var(--text-warm)] bg-[var(--bg-elevated)] border-b border-[var(--border-color)] whitespace-nowrap">
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((p) => {
|
||||
const isExpanded = expanded === p.patient_id;
|
||||
const gaps = patientFileGaps(p, mappedFields);
|
||||
const isPTN = p.rollup_status === "Plan Type Needed";
|
||||
const capped = p.plan_type_needed && !isPTN;
|
||||
const lastShip = (p.lines || [])
|
||||
.map((l) => l.shipment_date)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.pop();
|
||||
return (
|
||||
<Fragment key={p.patient_id}>
|
||||
<tr
|
||||
data-patient-row={p.patient_id}
|
||||
className={`border-b border-[var(--border-subtle)] cursor-pointer transition-colors hover:bg-[var(--row-hover)] ${
|
||||
!p.rollup_status ? "opacity-60" : ""
|
||||
} ${isExpanded ? "bg-[rgba(20,122,122,0.04)]" : ""}`}
|
||||
onClick={() => setExpanded(isExpanded ? null : p.patient_id)}
|
||||
>
|
||||
<td className="pl-[18px] py-[13px] w-[26px] text-[9px] text-[var(--text-warm)] align-middle">
|
||||
{isExpanded ? "▼" : "▶"}
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] align-middle">
|
||||
<span className="font-mono text-[12.5px] font-bold text-[var(--brand)]">
|
||||
{p.patient_id}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] text-[13px] text-[var(--text-primary)] align-middle">
|
||||
{(p.lines || []).map((l) => l.device_display).join(", ")}
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] text-[12.5px] text-[var(--text-secondary)] align-middle">
|
||||
{(p.lines || [])[0]?.payer || "—"}
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] align-middle">
|
||||
<VerdictBadge status={p.rollup_status} />
|
||||
{capped && (
|
||||
<div className="text-[10px] italic text-[var(--text-warm)] mt-[3px]">
|
||||
Capped: includes a Plan Type Needed line
|
||||
</div>
|
||||
)}
|
||||
{gaps.length > 0 && !isPTN && (
|
||||
<div className="text-[10px] italic text-[#6B4F9A] mt-[3px]">
|
||||
Data gap — {gaps.map((g) => DOC_LABELS[g]).join(", ")} column not provided in the file
|
||||
</div>
|
||||
)}
|
||||
{!p.rollup_status && (
|
||||
<div className="text-[10px] italic text-[var(--text-warm)] mt-[3px]">
|
||||
Non-CGM device — displayed, not graded, excluded from counts
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] font-mono text-[12px] text-[var(--text-warm)] align-middle">
|
||||
{lastShip || "—"}
|
||||
</td>
|
||||
<td className="px-[18px] py-[13px] align-middle" onClick={(e) => e.stopPropagation()}>
|
||||
{isPTN && (
|
||||
<button
|
||||
onClick={onImportClick}
|
||||
className="inline-flex items-center gap-[5px] text-[11px] font-semibold text-[#6B4F9A] bg-[rgba(122,94,160,0.10)] border border-[#9B8EC4] rounded-[5px] px-[11px] py-[4px] cursor-pointer"
|
||||
>
|
||||
◆ Map the plan type
|
||||
</button>
|
||||
)}
|
||||
{gaps.length > 0 && !isPTN && (
|
||||
<span className="inline-flex items-center gap-[4px] text-[10px] font-bold text-[#6B4F9A] bg-[rgba(122,94,160,0.10)] border border-[#9B8EC4] rounded-[4px] px-[7px] py-[1px]">
|
||||
◇ Fix the file, not the phone
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{isExpanded &&
|
||||
(p.lines || []).map((line) => (
|
||||
<tr key={`${p.patient_id}:${line.device_type}:${line.plan_type}`} className="bg-[var(--bg-elevated)] border-b border-[var(--border-subtle)]">
|
||||
<td colSpan={7} className="py-0">
|
||||
<DeviceLine
|
||||
patient={p}
|
||||
line={line}
|
||||
mappedFields={mappedFields}
|
||||
getToken={getToken}
|
||||
onRecomputed={(rec) => applyRecompute(p.patient_id, rec)}
|
||||
onConfirmVisit={() =>
|
||||
setConfirming({ record: lineToModalRecord(p, line), patientId: p.patient_id })
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{visible.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-[22px] py-8 text-center text-[var(--text-warm)]">
|
||||
No patients match this filter.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-[22px] py-3 border-t border-[var(--border-subtle)] text-[11px] text-[var(--text-warm)] bg-[var(--bg-elevated)]">
|
||||
<span className="text-[var(--text-secondary)]">
|
||||
PHI-safe — patient names and DOBs never stored. Crosswalk: patient_id only.
|
||||
</span>
|
||||
<span>
|
||||
{visible.length} of {patients.length} patients shown
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{confirming && (
|
||||
<ConfirmVisitModal
|
||||
record={confirming.record}
|
||||
onClose={() => setConfirming(null)}
|
||||
onConfirmed={(patientId, updated) => applyRecompute(patientId, updated)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceLine({ patient, line, mappedFields, getToken, onRecomputed, onConfirmVisit }) {
|
||||
const items = line.doc_items || [];
|
||||
const visitItem = items.find((i) => i.doc_type === "visit");
|
||||
const showConfirmVisit = line.gradeable && visitItem && !visitItem.satisfied;
|
||||
return (
|
||||
<div className="pl-[56px] pr-[22px] py-[11px] border-l-4 border-[rgba(20,122,122,0.16)] ml-[18px]">
|
||||
<div className="flex items-center gap-[10px] mb-[8px]">
|
||||
<span className="text-[12px] font-bold text-[var(--text-primary)]">{line.device_display}</span>
|
||||
<span className="text-[10.5px] text-[var(--text-warm)] bg-[var(--bg-card)] border border-[var(--border-color)] rounded px-[7px]">
|
||||
{line.plan_type || "Plan type needed"}
|
||||
</span>
|
||||
<VerdictBadge status={line.line_status} />
|
||||
{line.timing_flag && (
|
||||
<span className="text-[10.5px] text-[var(--text-warm)] italic">
|
||||
{TIMING_HINTS[line.timing_flag] || line.timing_flag}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto font-mono text-[10px] text-[var(--text-warm)]">
|
||||
{line.order_numbers?.length ? `Orders: ${line.order_numbers.join(", ")} · ` : ""}
|
||||
{line.hcpcs_codes?.length ? `HCPCS: ${line.hcpcs_codes.join(", ")} · ` : ""}
|
||||
qty {line.total_quantity}
|
||||
{line.prior_shipment_count > 0 && ` · ${line.prior_shipment_count} prior shipment${line.prior_shipment_count !== 1 ? "s" : ""}`}
|
||||
</span>
|
||||
</div>
|
||||
{line.gradeable && items.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-y-[6px]">
|
||||
{items.map((item) => (
|
||||
<ChecklistItem
|
||||
key={item.doc_type}
|
||||
patient={patient}
|
||||
line={line}
|
||||
item={item}
|
||||
fileGap={itemIsFileGap(item, mappedFields)}
|
||||
getToken={getToken}
|
||||
onRecomputed={onRecomputed}
|
||||
/>
|
||||
))}
|
||||
{showConfirmVisit && (
|
||||
<button
|
||||
onClick={onConfirmVisit}
|
||||
className="self-start ml-auto bg-transparent border border-[var(--accent)] text-[var(--accent-text)] px-[10px] py-[3px] rounded-md text-[11px] cursor-pointer whitespace-nowrap hover:bg-[rgba(203,107,32,0.1)]"
|
||||
>
|
||||
Confirm Visit →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11px] italic text-[var(--text-warm)]">
|
||||
Non-CGM device — no readiness actions.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChecklistItem({ patient, line, item, fileGap, getToken, onRecomputed }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const cyclable = item.doc_type === "swo" ? SWO_CYCLE : item.doc_type === "pa" ? PA_CYCLE : null;
|
||||
|
||||
const icon = fileGap
|
||||
? { glyph: "◇", cls: "text-[#6B4F9A]" }
|
||||
: item.satisfied
|
||||
? item.required_state === "NOT_REQUIRED" && item.input_state === "ABSENT"
|
||||
? { glyph: "—", cls: "text-[var(--text-warm)]" }
|
||||
: { glyph: "✓", cls: "text-[#166534]" }
|
||||
: item.quality === "BAD"
|
||||
? { glyph: "✗", cls: "text-[#CC2222]" }
|
||||
: { glyph: "✗", cls: "text-[#CB6B20]" };
|
||||
|
||||
const valueText = fileGap
|
||||
? "Column not mapped"
|
||||
: item.value ||
|
||||
(item.required_state === "NOT_REQUIRED" ? "Not required" : "Not on file");
|
||||
|
||||
async function handleCycle() {
|
||||
if (!cyclable || saving || fileGap) return;
|
||||
const currentKey = Object.entries(DOC_STATUS_LABELS).find(([, v]) => v === item.value)?.[0];
|
||||
const idx = currentKey ? cyclable.indexOf(currentKey) : -1;
|
||||
const nextKey = cyclable[(idx + 1) % cyclable.length];
|
||||
setSaving(true);
|
||||
try {
|
||||
const token = getToken ? await getToken().catch(() => null) : null;
|
||||
const result = await updateDocStatus(
|
||||
patient.patient_id,
|
||||
item.doc_type,
|
||||
nextKey,
|
||||
null,
|
||||
null,
|
||||
token,
|
||||
line.device_type,
|
||||
(patient.lines || []).map(lineToEcho)
|
||||
);
|
||||
if (result?.lines) onRecomputed(result);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-[6px] basis-1/5 min-w-[170px] pr-[12px]">
|
||||
<span className={`text-[12px] font-extrabold w-[14px] text-center ${icon.cls}`}>{icon.glyph}</span>
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold text-[var(--text-primary)]">
|
||||
{DOC_LABELS[item.doc_type]}
|
||||
</div>
|
||||
{cyclable && !fileGap ? (
|
||||
<button
|
||||
onClick={handleCycle}
|
||||
disabled={saving}
|
||||
title={`Click to update ${DOC_LABELS[item.doc_type]} status (this device only)`}
|
||||
className={`text-[11px] font-medium bg-transparent border-none p-0 text-left cursor-pointer hover:underline underline-offset-2 disabled:opacity-50 ${
|
||||
item.satisfied ? "text-[#166534]" : "text-[#CB6B20]"
|
||||
}`}
|
||||
>
|
||||
{saving ? "Saving…" : valueText}
|
||||
</button>
|
||||
) : (
|
||||
<div className={`text-[11px] ${fileGap ? "text-[#6B4F9A] font-semibold" : item.satisfied ? "text-[var(--text-secondary)]" : "text-[#CB6B20] font-medium"}`}>
|
||||
{valueText}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[9.5px] text-[var(--text-warm)]">
|
||||
{item.rule_id ? `${item.rule_id}` : ""}
|
||||
{fileGap ? " · resolves at re-import or mapping review" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,23 +5,13 @@ const LEGEND_ITEMS = [
|
|||
{ color: "#22C55E", name: "Clear to Ship", meaning: "In resupply window. Ready to go.", check: true },
|
||||
];
|
||||
|
||||
// P6 additions (Kisa-approved 2026-07-07): the readiness states the nested
|
||||
// worklist introduces. Legacy surfaces keep the original four.
|
||||
const EXTENDED_ITEMS = [
|
||||
{ color: "#9B8EC4", name: "Plan Type Needed", meaning: "Map the plan type to grade documentation." },
|
||||
{ color: "#9B8EC4", name: "Data gap", meaning: "A required column was not in the file. Fix the import, not the patient.", diamond: true },
|
||||
];
|
||||
|
||||
export default function StatusLegend({ extended = false }) {
|
||||
const items = extended ? [...LEGEND_ITEMS, ...EXTENDED_ITEMS] : LEGEND_ITEMS;
|
||||
export default function StatusLegend() {
|
||||
return (
|
||||
<div className="flex items-center gap-x-5 gap-y-2 flex-wrap px-[22px] py-[10px] border-b border-[var(--border-subtle)] bg-[var(--bg-elevated)] font-body">
|
||||
{items.map((item) => (
|
||||
{LEGEND_ITEMS.map((item) => (
|
||||
<div key={item.name} className="flex items-center gap-[7px] whitespace-nowrap">
|
||||
{item.check
|
||||
? <span style={{ color: item.color, fontSize: "13px", fontWeight: 800, lineHeight: 1, width: "8px", textAlign: "center" }}>✓</span>
|
||||
: item.diamond
|
||||
? <span style={{ color: item.color, fontSize: "11px", fontWeight: 800, lineHeight: 1, width: "8px", textAlign: "center" }}>◇</span>
|
||||
: <span className="inline-block rounded-full shrink-0" style={{ width: "8px", height: "8px", backgroundColor: item.color }} />
|
||||
}
|
||||
<span className="text-[11.5px] font-semibold text-[var(--text-secondary)]">
|
||||
|
|
|
|||
|
|
@ -124,34 +124,13 @@ export async function confirmVisit(payload, token = null) {
|
|||
throw new Error(err.detail || `HTTP ${res.status}`);
|
||||
}
|
||||
const raw = await res.json();
|
||||
// Normalize to local shape, preserving the P4 recompute payload (the
|
||||
// re-rolled patient) when the request echoed the patient's lines.
|
||||
const local = apiRecordToLocal(raw);
|
||||
if (raw.rollup_status !== undefined) local.rollup_status = raw.rollup_status;
|
||||
if (raw.lines) local.lines = raw.lines;
|
||||
return local;
|
||||
// Normalize to local shape
|
||||
return apiRecordToLocal(raw);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the work queue as one CSV row per device coverage line (P6,
|
||||
* granularity locked 2026-07-07). Falls through to the caller on failure.
|
||||
*/
|
||||
export async function exportLinesFromBackend(lines, batchId = null, token = null) {
|
||||
const authHeader = token
|
||||
? { "Authorization": `Bearer ${token}` }
|
||||
: {};
|
||||
const resp = await fetch(`${BACKEND_URL}/api/export`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...authHeader },
|
||||
body: JSON.stringify({ records: [], lines, batch_id: batchId }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`API error ${resp.status}`);
|
||||
return resp.blob();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SWO or PA status for a patient.
|
||||
* @param {string} patientId
|
||||
|
|
@ -162,7 +141,7 @@ export async function exportLinesFromBackend(lines, batchId = null, token = null
|
|||
* @param {string|null} token - Clerk JWT
|
||||
* @returns {Promise<{patient_id, doc_type, status, display}|null>}
|
||||
*/
|
||||
export async function updateDocStatus(patientId, docType, status, statusDate = null, expiryDate = null, token = null, deviceType = "", lines = null) {
|
||||
export async function updateDocStatus(patientId, docType, status, statusDate = null, expiryDate = null, token = null, deviceType = "") {
|
||||
const authHeader = token
|
||||
? { "Authorization": `Bearer ${token}` }
|
||||
: {};
|
||||
|
|
@ -177,9 +156,6 @@ export async function updateDocStatus(patientId, docType, status, statusDate = n
|
|||
// Device scope (P4): an SWO/PA update applies to this device's line
|
||||
// only, never to every device the patient has.
|
||||
...(deviceType && { device_type: deviceType }),
|
||||
// Echo the patient's lines so the response carries the recomputed
|
||||
// {lines, rollup_status} from the same engine (P4 contract).
|
||||
...(lines && { lines }),
|
||||
...(statusDate && { status_date: statusDate }),
|
||||
...(expiryDate && { expiry_date: expiryDate }),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -722,46 +722,3 @@ def test_unrecognized_plan_value_warning_hashes_patient(caplog):
|
|||
hits = [r for r in caplog.records if "Unrecognized plan_type" in r.getMessage()]
|
||||
assert len(hits) == 1
|
||||
assert "PT-RAW-SECRET" not in hits[0].getMessage()
|
||||
|
||||
|
||||
def test_export_per_line_rows():
|
||||
"""P6 (Kisa-locked): the work-queue export writes one row per coverage
|
||||
line when lines are provided; legacy record export unchanged otherwise."""
|
||||
resp = client.post(
|
||||
"/api/export",
|
||||
json={
|
||||
"lines": [
|
||||
{
|
||||
"patient_id": "PT-EXP-1",
|
||||
"device_display": "Dexcom G7",
|
||||
"payer": "Medicare Part B",
|
||||
"plan_type": "medicare",
|
||||
"rollup_status": "Action Needed",
|
||||
"line_status": "Clear to Ship",
|
||||
"timing": "In resupply window",
|
||||
"order_numbers": "ORD-1, ORD-2",
|
||||
"hcpcs_codes": "A4239",
|
||||
"total_quantity": 3,
|
||||
"shipment_date": "2026-07-02",
|
||||
"swo": "On File",
|
||||
"visit": "2026-06-14",
|
||||
"pecos": "Enrolled",
|
||||
"pa": "Not required",
|
||||
"diagnosis": "E11.65",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
lines = body.strip().splitlines()
|
||||
assert lines[0].startswith("Patient ID,Device,Payer,Plan Type")
|
||||
assert len(lines) == 2
|
||||
assert '"ORD-1, ORD-2"' in lines[1] # joined order numbers stay one cell
|
||||
assert "Clear to Ship" in lines[1]
|
||||
|
||||
|
||||
def test_export_legacy_records_shape_unchanged():
|
||||
resp = client.post("/api/export", json={"records": [], "batch_id": None})
|
||||
assert resp.status_code == 200
|
||||
assert resp.text.strip().splitlines()[0].startswith("Order Number,HCPCS,Patient ID")
|
||||
|
|
|
|||
Loading…
Reference in a new issue