diff --git a/src/pages/Upload.tsx b/src/pages/Upload.tsx index ba4124d..99e631d 100644 --- a/src/pages/Upload.tsx +++ b/src/pages/Upload.tsx @@ -1,9 +1,12 @@ import { useMemo, useRef, useState } from "react"; import { AlertTriangle, + ArrowRight, CheckCircle2, ChevronRight, + CloudUpload, FileText, + Inbox, Loader2, Upload as UploadIcon, XCircle, @@ -11,7 +14,6 @@ import { import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { PageHeader } from "@/components/PageHeader"; import { Select, SelectContent, @@ -117,12 +119,18 @@ function StatPill({ }) { return (
-
{label}
+
+ {label} +
{value}
@@ -135,32 +143,54 @@ function ClaimCard837({ claim }: { claim: ClaimOutput }) { const passed = claim.validation.passed; const hasWarnings = claim.validation.warnings.length > 0; return ( -
+
{open ? ( -
+
0 ? (
-
Diagnoses
+
+ Diagnoses +
{claim.diagnoses.map((d, i) => ( @@ -212,11 +256,22 @@ function ClaimCard837({ claim }: { claim: ClaimOutput }) { {claim.service_lines.length > 0 ? (
-
Service lines
-
+
+ Service lines +
+
- - + + @@ -238,7 +293,12 @@ function ClaimCard837({ claim }: { claim: ClaimOutput }) { {(claim.validation.errors.length > 0 || claim.validation.warnings.length > 0) ? (
-
Validation
+
+ Validation +
    {claim.validation.errors.map((issue, i) => (
  • -
- + + - - - - @@ -306,31 +389,48 @@ function ClaimCard835({ claim }: { claim: ClaimPayment }) { const [open, setOpen] = useState(false); const passed = claim.service_payments.length > 0; return ( -
+
{open ? ( -
+
@@ -373,11 +484,22 @@ function ClaimCard835({ claim }: { claim: ClaimPayment }) { {claim.service_payments.length > 0 ? (
-
Service payments
-
+
+ Service payments +
+
# Code Mods{line.line_number} +
+ {line.line_number} + {line.procedure.qualifier}·{line.procedure.code} + {line.procedure.modifiers.length > 0 ? line.procedure.modifiers.join(", ") : "—"} + {line.service_date ?? "—"} + {line.units ? `${toNum(line.units)} ${line.unit_type ?? ""}`.trim() : "—"} + {fmt.usdDecimal(line.charge)}
- - + + @@ -404,24 +526,49 @@ function ClaimCard835({ claim }: { claim: ClaimPayment }) { function ServicePaymentRow({ svc }: { svc: ServicePayment }) { return ( - - - + + - - - - - @@ -547,260 +694,810 @@ export function Upload() { if (f) pickFile(f); } + // ----------------------------------------------------------------- + // Stagger choreography + // ----------------------------------------------------------------- + const heroDelay = 0; + const foldDelay = 220; + const titleDelay = 320; + const dropDelay = 460; + const streamDelay = 620; + const batchesDelay = 760; + return ( -
- Parse an X12 file} - subtitle="Upload an 837P professional claim or 835 ERA remittance file. The parser streams claims back as they're produced so the UI updates in real time." - /> - -
-
-
-
Kind
- -
-
-
Payer config
- -
-
- +
+ {/* ================================================================= + HERO — DARK EDITORIAL HEADER + Larger and bolder than the default PageHeader. The "X12" + wordmark sits italic as the page's anchor, and a ghost + "DROP" watermark fills the right side as a print-shop stamp. + ================================================================= */} +
+ {/* Ghost wordmark — a large faded "DROP" stamp behind the title */}
{ - e.preventDefault(); - setDragging(true); + aria-hidden="true" + className="pointer-events-none select-none absolute inset-x-0 top-[58%] -translate-y-1/2 whitespace-nowrap display text-center" + style={{ + fontSize: "clamp(160px, 22vw, 320px)", + letterSpacing: "-0.05em", + opacity: 0.045, + lineHeight: 1, + color: "hsl(var(--surface-ink))", }} - onDragLeave={() => setDragging(false)} - onDrop={onDrop} - onClick={() => inputRef.current?.click()} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") inputRef.current?.click(); - }} - className={cn( - "relative flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed transition-colors", - "px-6 py-12 min-h-[160px] cursor-pointer", - dragging - ? "border-accent bg-accent/5 ring-2 ring-accent/30" - : "border-border/60 bg-muted/20 hover:bg-muted/30 hover:border-border", - "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" - )} > - {/* Soft inner glow when dragging — a precision-instrument - "active" state. */} -
- -
- {file ? ( -
- - {file.name} - - · {formatBytes(file.size)} - -
- ) : ( - <> -
- {dragging ? "Release to upload" : "Drop a file here, or click to choose"} -
-
- .txt — the X12 837/835 file as exported from your clearinghouse -
- - )} - pickFile(e.target.files?.[0] ?? null)} - /> + DROP
-
-
- {api.isConfigured ? ( - - - Backend ready · {api.baseUrl} - - ) : ( - - - No backend configured — set VITE_API_BASE_URL to enable parsing - - )} -
-
- {file ? ( - - ) : null} - + · {api.isConfigured ? "ready" : "no backend"} +
+ +

+ Parse an{" "} + X12 +
+ file. +

+ +

+ Drop an 837P professional claim or 835 ERA remittance file. The + parser streams claims back as they're produced so the UI updates + in real time. +

+
+
+ + {/* ================================================================= + FOLD — TORN PAGE + ================================================================= */} +
+
+
+
+
+ {Array.from({ length: 48 }, (_, i) => ( + + ))} +
+
+ + ↘ + + + Drop & parse + + + ↙ +
- {(running || stream.items.length > 0) ? ( -
-
+ {/* ================================================================= + PAPER PLANE — DROP ZONE + Title block → dropzone → stream results → recent batches + ================================================================= */} +
+ {/* Paper grain */} +
\")", + backgroundSize: "160px 160px", + mixBlendMode: "multiply", + }} + /> + + {/* Title block */} +
+
+
-
- {running ? ( - - ) : ( - - )} - {kind === "837p" ? "Claims" : "Claim payments"} +
+ Ingestion · Sheet 01 +
+

+ Drop a file. +

+
+ The X12 837/835 file as exported from your clearinghouse. + The parser handles validation, line items, and service + payments on the way in.
-

- {totalSoFar} of {expectedTotal ?? "?"} parsed - {stream.failed > 0 - ? ` · ${stream.failed} failed validation` - : ""} -

-
-
- {expectedTotal ? `${progressPct}%` : "…"} +
+
+ Accepts +
+
+ .txt +
+
+ 837P · 835 +
+
+ + + {api.isConfigured ? "Backend ready" : "No backend"} +
-
progress
-
+
+ + {/* DROP ZONE — section § 01 */} +
+ {/* Folio */} +
+ + § 01 +
+ + The drop + +
+ +
+
+
+ Kind +
+ +
+
+
+ Payer config +
+ +
+
+ +
{ + e.preventDefault(); + setDragging(true); + }} + onDragLeave={() => setDragging(false)} + onDrop={onDrop} + onClick={() => inputRef.current?.click()} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") inputRef.current?.click(); + }} + className={cn( + "relative flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed transition-colors", + "px-6 py-14 min-h-[180px] cursor-pointer", + dragging + ? "border-[hsl(212_100%_45%)] bg-[hsl(212_85%_95%)] ring-2 ring-[hsl(212_100%_45%_/_0.25)]" + : "border-[hsl(30_14%_14%/_0.22)] bg-[hsl(36_22%_98%)] hover:bg-[hsl(36_22%_94%)] hover:border-[hsl(30_14%_14%/_0.35)]", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(212_100%_45%_/_0.4)]" + )} + > +
+ {dragging ? ( + + ) : ( + + )} +
+ {file ? ( +
+ + + {file.name} + + + · {formatBytes(file.size)} + +
+ ) : ( + <> +
+ {dragging + ? "Release to upload" + : "Drop a file here, or click to choose"} +
+
+ .txt — the X12 837/835 file as exported from your + clearinghouse +
+ + )} + pickFile(e.target.files?.[0] ?? null)} />
-
- {stream.items.length === 0 && running ? ( -
- Waiting for first claim… -
- ) : null} - {stream.items.map((item, i) => - item.kind === "837p" ? ( - +
+
+ {api.isConfigured ? ( + + + Backend ready · {api.baseUrl} + ) : ( - - ) - )} -
-
- ) : null} - - {parsedBatches.length > 0 ? ( -
-
Recent batches
-

- Parsed files, newest first. -

-
    - {parsedBatches.map((b) => ( -
  • + + No backend configured — set VITE_API_BASE_URL to enable + parsing + + )} +
+
+ {file ? ( + + ) : null} + +
+
+
+ + {/* STREAM RESULTS — section § 02 */} + {(running || stream.items.length > 0) ? ( +
+
+ + § 02 + +
+ + The stream + +
+ +
+
+
+
+ The stream
-
- {fmt.dateShort(b.parsedAt)} · {b.claimCount} {b.kind === "837p" ? "claims" : "payments"} +

+ {running ? ( + <> + Parsing{" "} + live. + + ) : ( + <> + Stream{" "} + complete. + + )} +

+
+
+
+
+ Parsed +
+
+ {totalSoFar} + + {" "} + / {expectedTotal ?? "?"} + +
+
+
+
+ Progress +
+
+ {expectedTotal ? `${progressPct}%` : "…"} +
-
-
- {b.passed} / {b.claimCount} +
+ +
+
+
+ +
+ {stream.items.length === 0 && running ? ( +
+ Waiting for first claim…
-
- passed + ) : null} + {stream.items.map((item, i) => + item.kind === "837p" ? ( + + ) : ( + + ) + )} +
+
+
+ ) : null} + + {/* RECENT BATCHES — section § 03 */} + {parsedBatches.length > 0 ? ( +
+
+ + § 03 + +
+ + The archive + +
+ +
+
+
+
+ The archive +
+

+ Recent batches. +

+
+
+
+ Total +
+
+ {parsedBatches.length}
- - ))} - +
+
    + {parsedBatches.map((b) => ( +
  • + + {b.kind === "837p" ? "837P" : "835"} + +
    +
    + {b.inputFilename} +
    +
    + {fmt.dateShort(b.parsedAt)} · {b.claimCount}{" "} + {b.kind === "837p" ? "claims" : "payments"} +
    +
    +
    +
    + {b.passed} / {b.claimCount} +
    +
    + passed +
    +
    +
  • + ))} +
+
+
+ ) : null} + + {/* Inbox prompt when nothing parsed yet */} + {parsedBatches.length === 0 && stream.items.length === 0 && !running ? ( +
+
+ +
+ No batches yet. +
+
+ Drop a file above to start a parse. +
+
+
+ ) : null} + + {/* Footer */} +
+ End of sheet 01 + Cyclone · Ingestion + + {parsedBatches.length}{" "} + {parsedBatches.length === 1 ? "batch" : "batches"} +
- ) : null} +
); }
# Code Mods
{svc.line_number} +
+ {svc.line_number} + {svc.procedure_qualifier}·{svc.procedure_code} + {svc.modifiers.length > 0 ? svc.modifiers.join(", ") : "—"} + {svc.service_date ?? "—"} + {svc.units ? `${toNum(svc.units)} ${svc.unit_type ?? ""}`.trim() : "—"} + {fmt.usdDecimal(svc.charge)} + {fmt.usdDecimal(svc.payment)}