@mindbill/react 0.21.0 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -153,14 +153,16 @@ The component includes the interaction model, not just the markup:
153
153
  - a responsive two-column review form (one column on narrow screens);
154
154
  - paste-friendly `MM/DD/YYYY` date fields and required-field asterisks;
155
155
  - ZIP-to-city/state completion through MindBill's authenticated postal directory;
156
- - complete, server-backed ICD-10 search, common-injury quick picks, and removable chips;
156
+ - complete, server-backed ICD-10 search with an alphabetized 100-code first page, automatic 100-code scroll paging, common-injury quick picks, and removable chips;
157
157
  - canonical claims-administrator search through the authenticated MindBill payer directory;
158
158
  - QME, AME, and Psych QME evaluation modes with medical-legal modifier defaults;
159
159
  - searchable workers-comp procedure/modifier controls, medical-legal fee-schedule amounts, totals, valid manual CPT/HCPCS entry, and an automatically maintained empty line;
160
- - removable source documents, a locked auto-attached practice W-9, and click, panel-drop, or whole-page PDF upload.
160
+ - removable source documents with new-tab previews, a locked auto-attached practice W-9, and a full-width click, panel-drop, or whole-page PDF upload area. Every visible source document is included until removed; uploaded PDFs do not require document-type tagging.
161
161
 
162
162
  Partners supply tenant-specific bootstrap data, one short-lived browser session callback, and the final server submission callback. Required fields, validation, ZIP lookup, ICD-10 and payer directories, service-line behavior, attachments, and submission UX stay inside `@mindbill/react`, so every integration receives the same billing workflow. Optional `diagnosisOptions`, `procedureOptions`, `modifierOptions`, and lookup callbacks extend or replace defaults when a partner has licensed or organization-specific data.
163
163
 
164
+ Validation is component-owned as well. A missing routing payer is highlighted immediately; Submit highlights every invalid control with a specific message, focuses and scrolls to the first problem, and continues validating as the user corrects the form.
165
+
164
166
  `BILL_SUBMISSION_REQUIRED_FIELDS` and `validateBillSubmission` expose the same contract for tests and non-visual integrations. The component never creates a draft; its callback fires only when the user submits a locally valid snapshot.
165
167
 
166
168
  `BillReviewForm` remains available for legacy integrations that already own a custom review model. New integrations should use `BillSubmissionForm`.
package/dist/index.d.ts CHANGED
@@ -453,6 +453,7 @@ type BillSubmissionSourceAttachment = {
453
453
  fileName: string;
454
454
  documentType: BillSubmissionDocumentType;
455
455
  description?: string;
456
+ /** @deprecated Source attachments shown in the form are included until removed. */
456
457
  selected?: boolean;
457
458
  /** Auto-attached documents such as a practice W-9 cannot be deselected. */
458
459
  autoAttached?: boolean;
@@ -480,7 +481,7 @@ type BillSubmissionFormProps = {
480
481
  fetch?: typeof globalThis.fetch;
481
482
  onSearchClaimsAdministrators?: (query: string, claimNumber?: string) => Promise<BillReviewPayer$1[]>;
482
483
  diagnosisOptions?: BillSubmissionDiagnosisOption[];
483
- onSearchDiagnoses?: (query: string) => Promise<BillSubmissionDiagnosisOption[]>;
484
+ onSearchDiagnoses?: (query: string, limit?: number, offset?: number) => Promise<BillSubmissionDiagnosisOption[]>;
484
485
  onLookupPostalCode?: (postalCode: string) => Promise<BillSubmissionPostalPlace | null>;
485
486
  procedureOptions?: BillSubmissionProcedureOption[];
486
487
  modifierOptions?: BillSubmissionModifierOption[];
package/dist/index.js CHANGED
@@ -1320,7 +1320,13 @@ var documentLabels = {
1320
1320
  var MAX_PDF_BYTES = 25 * 1024 * 1024;
1321
1321
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
1322
1322
  var MAX_DOCUMENTS = 25;
1323
+ var DIAGNOSIS_PAGE_SIZE = 100;
1323
1324
  var EMPTY_ATTACHMENTS = [];
1325
+ function previewUploadedPdf(file) {
1326
+ const url = URL.createObjectURL(file);
1327
+ window.open(url, "_blank", "noopener,noreferrer");
1328
+ window.setTimeout(() => URL.revokeObjectURL(url), 6e4);
1329
+ }
1324
1330
  function blankLine() {
1325
1331
  return { code: "", modifiers: [], units: 1 };
1326
1332
  }
@@ -1384,19 +1390,19 @@ function cloneBill(bill) {
1384
1390
  }
1385
1391
  function validateBillSubmission(bill) {
1386
1392
  const errors = {};
1387
- const required = (path, value) => {
1388
- if (typeof value !== "string" || !value.trim()) errors[path] = "Required";
1393
+ const required = (path, value, message) => {
1394
+ if (typeof value !== "string" || !value.trim()) errors[path] = message;
1389
1395
  };
1390
- required("patient.firstName", bill.patient.firstName);
1391
- required("patient.lastName", bill.patient.lastName);
1392
- required("patient.dateOfBirth", bill.patient.dateOfBirth);
1393
- required("patient.address.line1", bill.patient.address.line1);
1394
- required("patient.address.city", bill.patient.address.city);
1395
- required("patient.address.state", bill.patient.address.state);
1396
- required("patient.address.postalCode", bill.patient.address.postalCode);
1397
- required("claim.claimNumber", bill.claim.claimNumber);
1398
- required("claim.claimsAdministrator", bill.claim.claimsAdministrator?.name);
1399
- required("service.date", bill.service.date);
1396
+ required("patient.firstName", bill.patient.firstName, "Enter the patient's first name.");
1397
+ required("patient.lastName", bill.patient.lastName, "Enter the patient's last name.");
1398
+ required("patient.dateOfBirth", bill.patient.dateOfBirth, "Enter the patient's date of birth.");
1399
+ required("patient.address.line1", bill.patient.address.line1, "Enter the patient's street address.");
1400
+ required("patient.address.city", bill.patient.address.city, "Enter the patient's city.");
1401
+ required("patient.address.state", bill.patient.address.state, "Enter the patient's 2-letter state code.");
1402
+ required("patient.address.postalCode", bill.patient.address.postalCode, "Enter the patient's ZIP code.");
1403
+ required("claim.claimNumber", bill.claim.claimNumber, "Enter the claim number.");
1404
+ required("claim.claimsAdministrator", bill.claim.claimsAdministrator?.name, "Select a claims administrator so MindBill can route this bill.");
1405
+ required("service.date", bill.service.date, "Enter the date of service.");
1400
1406
  if (bill.claim.claimsAdministrator?.name && !bill.claim.claimsAdministrator.id) {
1401
1407
  errors["claim.claimsAdministrator"] = "Select a claims administrator from the payer directory.";
1402
1408
  }
@@ -1405,7 +1411,7 @@ function validateBillSubmission(bill) {
1405
1411
  const lines = submittedLines(bill.serviceLines);
1406
1412
  if (!lines.length) errors.serviceLines = "Add at least one service line";
1407
1413
  lines.forEach((line, index) => {
1408
- required(`serviceLines.${index}.code`, line.code);
1414
+ required(`serviceLines.${index}.code`, line.code, "Select a procedure code.");
1409
1415
  if (!Number.isInteger(line.units) || (line.units ?? 0) < 1) errors[`serviceLines.${index}.units`] = "Enter at least 1 unit";
1410
1416
  if (bill.billingMode === "professional" && (!Number.isFinite(line.charge) || (line.charge ?? 0) < 0)) errors[`serviceLines.${index}.charge`] = "Enter the billed charge";
1411
1417
  });
@@ -1414,25 +1420,25 @@ function validateBillSubmission(bill) {
1414
1420
  var css = `
1415
1421
  .mbsf{display:grid;gap:20px;color:var(--mb-text);font-family:var(--mb-font);font-size:15px}.mbsf *{box-sizing:border-box}
1416
1422
  .mbsf-head,.mbsf-section-head,.mbsf-attach-row,.mbsf-actions{display:flex;align-items:center;justify-content:space-between;gap:16px}.mbsf-title{margin:0;font-size:24px}.mbsf-copy,.mbsf-help{color:var(--mb-muted);margin:5px 0 0}.mbsf-required{font-size:13px;color:var(--mb-muted);white-space:nowrap}.mbsf-star,.mbsf-error{color:var(--mb-danger)}
1417
- .mbsf-card{min-width:0;margin:0;padding:24px;border:1px solid var(--mb-border);border-radius:var(--mb-radius);background:var(--mb-surface);box-shadow:var(--mb-shadow)}.mbsf-legend{padding:0 10px;font-size:18px;font-weight:760}.mbsf-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 24px}.mbsf-span{grid-column:1/-1}.mbsf-field{display:grid;align-content:start;gap:7px;min-width:0}.mbsf-label{font-weight:680}.mbsf-input,.mbsf-select{width:100%;min-height:46px;padding:10px 12px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);background:var(--mb-input);color:var(--mb-text);font:inherit}.mbsf-input:focus,.mbsf-select:focus{outline:3px solid color-mix(in srgb,var(--mb-accent) 22%,transparent);border-color:var(--mb-accent)}
1418
- .mbsf-combo{position:relative}.mbsf-menu{position:absolute;z-index:20;top:calc(100% + 5px);left:0;right:0;max-height:min(300px,42vh);overflow:auto;overscroll-behavior:contain;padding:7px;border:1px solid var(--mb-border);border-radius:12px;background:var(--mb-surface);box-shadow:0 14px 35px rgba(17,38,49,.16)}.mbsf-option{display:grid;width:100%;gap:2px;padding:10px;border:0;border-radius:8px;background:transparent;color:var(--mb-text);font:inherit;text-align:left;cursor:pointer}.mbsf-option:hover,.mbsf-option:focus{background:color-mix(in srgb,var(--mb-accent) 9%,var(--mb-surface))}.mbsf-option small{color:var(--mb-muted)}
1423
+ .mbsf-card{min-width:0;margin:0;padding:24px;border:1px solid var(--mb-border);border-radius:var(--mb-radius);background:var(--mb-surface);box-shadow:var(--mb-shadow)}.mbsf-legend{padding:0 10px;font-size:18px;font-weight:760}.mbsf-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 24px}.mbsf-span{grid-column:1/-1}.mbsf-field{display:grid;align-content:start;gap:7px;min-width:0}.mbsf-label{font-weight:680}.mbsf-input,.mbsf-select{width:100%;min-height:46px;padding:10px 12px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);background:var(--mb-input);color:var(--mb-text);font:inherit}.mbsf-input:focus,.mbsf-select:focus{outline:3px solid color-mix(in srgb,var(--mb-accent) 22%,transparent);border-color:var(--mb-accent)}.mbsf-field[data-invalid=true] .mbsf-input,.mbsf-field[data-invalid=true] .mbsf-select,.mbsf-invalid-control .mbsf-input{border-color:var(--mb-danger);background:color-mix(in srgb,var(--mb-danger) 4%,var(--mb-input))}.mbsf-field[data-invalid=true] .mbsf-input:focus,.mbsf-field[data-invalid=true] .mbsf-select:focus{outline-color:color-mix(in srgb,var(--mb-danger) 24%,transparent)}
1424
+ .mbsf-combo{position:relative}.mbsf-menu{position:absolute;z-index:20;top:calc(100% + 5px);left:0;right:0;max-height:min(360px,46vh);overflow:auto;overscroll-behavior:contain;padding:7px;border:1px solid var(--mb-border);border-radius:12px;background:var(--mb-surface);box-shadow:0 14px 35px rgba(17,38,49,.16)}.mbsf-option{display:grid;width:100%;gap:2px;padding:10px;border:0;border-radius:8px;background:transparent;color:var(--mb-text);font:inherit;text-align:left;cursor:pointer}.mbsf-option:hover,.mbsf-option:focus{background:color-mix(in srgb,var(--mb-accent) 9%,var(--mb-surface))}.mbsf-option small{color:var(--mb-muted)}.mbsf-menu-status{padding:12px;text-align:center;color:var(--mb-muted);font-size:13px}
1419
1425
  .mbsf-chips,.mbsf-quick-picks{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:8px}.mbsf-chip,.mbsf-quick-pick{display:inline-flex;align-items:center;gap:7px;padding:5px 9px;border:1px solid var(--mb-border);border-radius:999px;background:var(--mb-input)}.mbsf-chip button{border:0;background:transparent;color:var(--mb-muted);cursor:pointer;font:inherit}.mbsf-quick-pick{color:var(--mb-text);font:inherit;cursor:pointer}.mbsf-quick-pick[data-selected=true]{border-color:var(--mb-accent);color:var(--mb-accent)}
1420
1426
  .mbsf-segments{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);overflow:hidden}.mbsf-segment{min-height:44px;border:0;border-right:1px solid var(--mb-border);background:var(--mb-input);color:var(--mb-text);font:inherit;font-weight:700;cursor:pointer}.mbsf-segment:last-child{border-right:0}.mbsf-segment[aria-pressed=true]{background:var(--mb-accent);color:var(--mb-accent-contrast)}
1421
- .mbsf-lines{margin-top:18px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);overflow:visible}.mbsf-line-head,.mbsf-line{display:grid;grid-template-columns:minmax(190px,1.05fr) minmax(220px,1.5fr) 100px 120px 42px;gap:12px;align-items:start;padding:12px}.mbsf-line-head{color:var(--mb-muted);font-size:13px;font-weight:700;border-bottom:1px solid var(--mb-border)}.mbsf-line{border-bottom:1px solid var(--mb-border)}.mbsf-line:last-child{border-bottom:0}.mbsf-money{padding-top:12px;text-align:right;font-variant-numeric:tabular-nums}.mbsf-total{display:flex;justify-content:flex-end;gap:45px;padding:16px 56px 16px 16px;font-size:17px;font-weight:760}
1422
- .mbsf-icon-btn{width:40px;height:42px;border:0;background:transparent;color:var(--mb-text);font-size:22px;cursor:pointer}.mbsf-secondary{min-height:40px;padding:8px 14px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);background:var(--mb-surface);color:var(--mb-text);font:inherit;font-weight:680;cursor:pointer}.mbsf-attach-list{display:grid;gap:10px;margin-bottom:18px}.mbsf-attach-row{padding:14px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius)}.mbsf-attach-row[data-auto=true]{border-color:color-mix(in srgb,#159447 45%,var(--mb-border));background:color-mix(in srgb,#159447 5%,var(--mb-surface))}.mbsf-file{min-width:0}.mbsf-file strong{overflow-wrap:anywhere}.mbsf-badge{display:inline-block;margin-left:8px;padding:2px 7px;border:1px solid var(--mb-border);border-radius:7px;color:var(--mb-muted);font-size:12px;font-weight:600}.mbsf-drop{display:grid;place-items:center;min-height:180px;padding:24px;border:2px dashed color-mix(in srgb,var(--mb-muted) 55%,transparent);border-radius:var(--mb-control-radius);background:color-mix(in srgb,var(--mb-accent) 3%,var(--mb-surface));text-align:center;cursor:pointer}.mbsf-drop[data-active=true]{border-color:var(--mb-accent);background:color-mix(in srgb,var(--mb-accent) 10%,var(--mb-surface))}.mbsf-alert{padding:12px 14px;border-radius:var(--mb-control-radius);background:color-mix(in srgb,var(--mb-danger) 10%,transparent);color:var(--mb-danger)}.mbsf-actions{justify-content:flex-end}.mbsf-submit{min-width:180px;min-height:48px;padding:11px 24px;border:0;border-radius:var(--mb-control-radius);background:var(--mb-accent);color:var(--mb-accent-contrast);font:inherit;font-weight:780;cursor:pointer}
1423
- @media(max-width:820px){.mbsf{gap:16px}.mbsf-grid{grid-template-columns:1fr}.mbsf-span{grid-column:auto}.mbsf-card{padding:18px 16px}.mbsf-line-head{display:none}.mbsf-line{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 86px;gap:14px;padding:18px 16px}.mbsf-line>div:before{display:block;margin-bottom:6px;color:var(--mb-muted);font-size:12px;font-weight:700;content:attr(data-label)}.mbsf-line>div:nth-child(1),.mbsf-line>div:nth-child(2){grid-column:1/-1}.mbsf-money{align-self:end;padding:0 0 12px;text-align:right}.mbsf-line .mbsf-icon-btn{position:absolute;right:8px;bottom:3px}.mbsf-total{padding:16px 18px;gap:24px}.mbsf-head{align-items:flex-start}.mbsf-segments{grid-template-columns:repeat(3,minmax(0,1fr))}.mbsf-segment{min-width:0;padding:8px 4px;border-right:1px solid var(--mb-border);border-bottom:0;font-size:13px}.mbsf-segment:last-child{border-right:0}.mbsf-attach-row{align-items:flex-start;flex-wrap:wrap}.mbsf-actions{position:sticky;bottom:86px;z-index:10}.mbsf-submit{width:100%}}
1427
+ .mbsf-lines{margin-top:18px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);overflow:visible}.mbsf-lines[data-invalid=true]{border-color:var(--mb-danger)}.mbsf-line-head,.mbsf-line{display:grid;grid-template-columns:minmax(190px,1.05fr) minmax(220px,1.5fr) 100px 120px 42px;gap:12px;align-items:start;padding:12px}.mbsf-line-head{color:var(--mb-muted);font-size:13px;font-weight:700;border-bottom:1px solid var(--mb-border)}.mbsf-line{border-bottom:1px solid var(--mb-border)}.mbsf-line:last-child{border-bottom:0}.mbsf-line [data-invalid=true] .mbsf-input{border-color:var(--mb-danger);background:color-mix(in srgb,var(--mb-danger) 4%,var(--mb-input))}.mbsf-money{padding-top:12px;text-align:right;font-variant-numeric:tabular-nums}.mbsf-total{display:flex;justify-content:flex-end;gap:45px;padding:16px 56px 16px 16px;font-size:17px;font-weight:760}
1428
+ .mbsf-icon-btn{width:40px;height:42px;border:0;background:transparent;color:var(--mb-text);font-size:22px;cursor:pointer}.mbsf-secondary{min-height:40px;padding:8px 14px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);background:var(--mb-surface);color:var(--mb-text);font:inherit;font-weight:680;cursor:pointer}.mbsf-attach-list{display:grid;gap:10px;margin-bottom:18px}.mbsf-attach-row{padding:14px;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius)}.mbsf-attach-row[data-auto=true]{border-color:color-mix(in srgb,#159447 45%,var(--mb-border));background:color-mix(in srgb,#159447 5%,var(--mb-surface))}.mbsf-attach-main{display:flex;align-items:center;gap:12px;min-width:0;flex:1}.mbsf-attach-actions{display:flex;align-items:center;gap:6px;flex:0 0 auto}.mbsf-file{min-width:0}.mbsf-file strong{overflow-wrap:anywhere}.mbsf-badge{display:inline-block;margin-left:8px;padding:2px 7px;border:1px solid var(--mb-border);border-radius:7px;color:var(--mb-muted);font-size:12px;font-weight:600}.mbsf-drop{display:grid;width:100%;place-items:center;min-height:210px;padding:30px;border:2px dashed color-mix(in srgb,var(--mb-muted) 55%,transparent);border-radius:var(--mb-control-radius);background:color-mix(in srgb,var(--mb-accent) 3%,var(--mb-surface));color:var(--mb-text);font:inherit;text-align:center;cursor:pointer}.mbsf-drop[data-active=true]{border-color:var(--mb-accent);background:color-mix(in srgb,var(--mb-accent) 10%,var(--mb-surface))}.mbsf-alert{padding:12px 14px;border-radius:var(--mb-control-radius);background:color-mix(in srgb,var(--mb-danger) 10%,transparent);color:var(--mb-danger)}.mbsf-actions{justify-content:flex-end}.mbsf-submit{min-width:180px;min-height:48px;padding:11px 24px;border:0;border-radius:var(--mb-control-radius);background:var(--mb-accent);color:var(--mb-accent-contrast);font:inherit;font-weight:780;cursor:pointer}
1429
+ @media(max-width:820px){.mbsf{gap:16px}.mbsf-grid{grid-template-columns:1fr}.mbsf-span{grid-column:auto}.mbsf-card{padding:18px 16px}.mbsf-line-head{display:none}.mbsf-line{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 86px;gap:14px;padding:18px 16px}.mbsf-line>div:before{display:block;margin-bottom:6px;color:var(--mb-muted);font-size:12px;font-weight:700;content:attr(data-label)}.mbsf-line>div:nth-child(1),.mbsf-line>div:nth-child(2){grid-column:1/-1}.mbsf-money{align-self:end;padding:0 0 12px;text-align:right}.mbsf-line .mbsf-icon-btn{position:absolute;right:8px;bottom:3px}.mbsf-total{padding:16px 18px;gap:24px}.mbsf-head{align-items:flex-start}.mbsf-segments{grid-template-columns:repeat(3,minmax(0,1fr))}.mbsf-segment{min-width:0;padding:8px 4px;border-right:1px solid var(--mb-border);border-bottom:0;font-size:13px}.mbsf-segment:last-child{border-right:0}.mbsf-attach-row{align-items:flex-start;flex-wrap:wrap}.mbsf-attach-main{align-items:flex-start}.mbsf-attach-actions{margin-left:auto}.mbsf-drop{min-height:190px;padding:24px 18px}.mbsf-actions{position:sticky;bottom:86px;z-index:10}.mbsf-submit{width:100%}}
1424
1430
  `;
1425
1431
  function RequiredMark() {
1426
1432
  return /* @__PURE__ */ jsx3("span", { className: "mbsf-star", children: " *" });
1427
1433
  }
1428
- function Field2({ label, required, error, span, children }) {
1429
- return /* @__PURE__ */ jsxs3("label", { className: `mbsf-field${span ? " mbsf-span" : ""}`, children: [
1434
+ function Field2({ label, required, error, path, span, children }) {
1435
+ return /* @__PURE__ */ jsxs3("label", { className: `mbsf-field${span ? " mbsf-span" : ""}`, "data-field-path": path, "data-invalid": Boolean(error), children: [
1430
1436
  /* @__PURE__ */ jsxs3("span", { className: "mbsf-label", children: [
1431
1437
  label,
1432
1438
  required ? /* @__PURE__ */ jsx3(RequiredMark, {}) : null
1433
1439
  ] }),
1434
1440
  children,
1435
- error ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: error }) : null
1441
+ error ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", role: "alert", children: error }) : null
1436
1442
  ] });
1437
1443
  }
1438
1444
  function TextDateInput({ value, onChange, disabled, required, ariaLabel }) {
@@ -1454,7 +1460,7 @@ function TextDateInput({ value, onChange, disabled, required, ariaLabel }) {
1454
1460
  invalid ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: "Use MM/DD/YYYY" }) : null
1455
1461
  ] });
1456
1462
  }
1457
- function ComboBox({ value, placeholder, options, disabled, loading, onQuery, onSelect, createOption, ariaLabel }) {
1463
+ function ComboBox({ value, placeholder, options, disabled, loading, loadingMore, invalid, onOpen, onQuery, onEndReached, onSelect, createOption, ariaLabel }) {
1458
1464
  const [open, setOpen] = useState2(false);
1459
1465
  const [query, setQuery] = useState2("");
1460
1466
  const visible = useMemo2(() => {
@@ -1465,24 +1471,50 @@ function ComboBox({ value, placeholder, options, disabled, loading, onQuery, onS
1465
1471
  }, [createOption, options, query]);
1466
1472
  const showMenu = open && (loading || visible.length > 0 || query.trim().length >= 2);
1467
1473
  return /* @__PURE__ */ jsxs3("div", { className: "mbsf-combo", children: [
1468
- /* @__PURE__ */ jsx3("input", { className: "mbsf-input", role: "combobox", "aria-label": ariaLabel, "aria-expanded": showMenu, autoComplete: "off", disabled, placeholder, value: open ? query : value, onFocus: () => {
1474
+ /* @__PURE__ */ jsx3("input", { className: "mbsf-input", role: "combobox", "aria-label": ariaLabel, "aria-invalid": invalid, "aria-expanded": showMenu, autoComplete: "off", disabled, placeholder, value: open ? query : value, onFocus: () => {
1469
1475
  setOpen(true);
1470
1476
  setQuery("");
1477
+ onOpen?.();
1471
1478
  }, onChange: (event) => {
1472
1479
  setOpen(true);
1473
1480
  setQuery(event.target.value);
1474
1481
  onQuery?.(event.target.value);
1475
1482
  }, onBlur: () => setTimeout(() => setOpen(false), 120) }),
1476
- showMenu ? /* @__PURE__ */ jsx3("div", { className: "mbsf-menu", role: "listbox", children: loading ? /* @__PURE__ */ jsx3("div", { className: "mbsf-option", children: "Searching\u2026" }) : visible.length ? visible.map((option) => /* @__PURE__ */ jsxs3("button", { className: "mbsf-option", type: "button", role: "option", onMouseDown: (event) => event.preventDefault(), onClick: () => {
1477
- onSelect(option);
1478
- setOpen(false);
1479
- setQuery("");
1483
+ showMenu ? /* @__PURE__ */ jsxs3("div", { className: "mbsf-menu", role: "listbox", onScroll: (event) => {
1484
+ const menu = event.currentTarget;
1485
+ if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120) onEndReached?.();
1480
1486
  }, children: [
1481
- /* @__PURE__ */ jsx3("strong", { children: option.label }),
1482
- option.detail ? /* @__PURE__ */ jsx3("small", { children: option.detail }) : null
1483
- ] }, option.id)) : /* @__PURE__ */ jsx3("div", { className: "mbsf-option", children: "No matches" }) }) : null
1487
+ loading && !visible.length ? /* @__PURE__ */ jsx3("div", { className: "mbsf-menu-status", children: "Loading codes\u2026" }) : visible.length ? visible.map((option) => /* @__PURE__ */ jsxs3("button", { className: "mbsf-option", type: "button", role: "option", onMouseDown: (event) => event.preventDefault(), onClick: () => {
1488
+ onSelect(option);
1489
+ setOpen(false);
1490
+ setQuery("");
1491
+ }, children: [
1492
+ /* @__PURE__ */ jsx3("strong", { children: option.label }),
1493
+ option.detail ? /* @__PURE__ */ jsx3("small", { children: option.detail }) : null
1494
+ ] }, option.id)) : /* @__PURE__ */ jsx3("div", { className: "mbsf-option", children: "No matches" }),
1495
+ loadingMore ? /* @__PURE__ */ jsx3("div", { className: "mbsf-menu-status", children: "Loading more\u2026" }) : null
1496
+ ] }) : null
1484
1497
  ] });
1485
1498
  }
1499
+ function focusFirstInvalid(form) {
1500
+ const field = form.querySelector('[data-invalid="true"]');
1501
+ if (!field) return;
1502
+ const control = field.querySelector('input,select,textarea,button,[tabindex]:not([tabindex="-1"])');
1503
+ control?.focus({ preventScroll: true });
1504
+ const fieldRect = field.getBoundingClientRect();
1505
+ let scrollParent = field.parentElement;
1506
+ while (scrollParent) {
1507
+ const style = window.getComputedStyle(scrollParent);
1508
+ if (/(auto|scroll)/.test(style.overflowY) && scrollParent.scrollHeight > scrollParent.clientHeight) break;
1509
+ scrollParent = scrollParent.parentElement;
1510
+ }
1511
+ if (scrollParent) {
1512
+ const parentRect = scrollParent.getBoundingClientRect();
1513
+ scrollParent.scrollTo({ top: Math.max(0, scrollParent.scrollTop + fieldRect.top - parentRect.top - 24), behavior: "smooth" });
1514
+ } else {
1515
+ window.scrollTo({ top: Math.max(0, window.scrollY + fieldRect.top - 24), behavior: "smooth" });
1516
+ }
1517
+ }
1486
1518
  function customProcedureOption(query) {
1487
1519
  const code = query.trim().toUpperCase().replace(/\s+/g, "");
1488
1520
  if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code)) return null;
@@ -1493,6 +1525,9 @@ function mergeOptions(defaults, supplied) {
1493
1525
  supplied?.forEach((item) => map.set(item.code.toUpperCase(), item));
1494
1526
  return [...map.values()];
1495
1527
  }
1528
+ function mergeDiagnosisOptions(options) {
1529
+ return [...new Map(options.map((item) => [item.code.toUpperCase(), item])).values()].sort((left, right) => left.code.localeCompare(right.code, void 0, { numeric: true }));
1530
+ }
1496
1531
  function BillSubmissionForm({
1497
1532
  initialBill,
1498
1533
  attachments = EMPTY_ATTACHMENTS,
@@ -1516,19 +1551,26 @@ function BillSubmissionForm({
1516
1551
  description = "Review the bill details, add attachments, and submit."
1517
1552
  }) {
1518
1553
  const [bill, setBill] = useState2(() => cloneBill(initialBill));
1519
- const [selectedIds, setSelectedIds] = useState2(() => attachments.filter((item) => item.selected !== false || item.autoAttached || item.documentType === "w9").map((item) => item.id));
1554
+ const [selectedIds, setSelectedIds] = useState2(() => attachments.map((item) => item.id));
1520
1555
  const [removedSourceIds, setRemovedSourceIds] = useState2([]);
1521
1556
  const [uploads, setUploads] = useState2([]);
1522
1557
  const [errors, setErrors] = useState2({});
1558
+ const [validationActive, setValidationActive] = useState2(false);
1523
1559
  const [formError, setFormError] = useState2(null);
1524
1560
  const [submitting, setSubmitting] = useState2(false);
1525
1561
  const [payerResults, setPayerResults] = useState2([]);
1526
1562
  const [payerLoading, setPayerLoading] = useState2(false);
1527
- const [diagnosisResults, setDiagnosisResults] = useState2(diagnosisOptions);
1563
+ const [diagnosisResults, setDiagnosisResults] = useState2([]);
1528
1564
  const [diagnosisLoading, setDiagnosisLoading] = useState2(false);
1565
+ const [diagnosisLoadingMore, setDiagnosisLoadingMore] = useState2(false);
1566
+ const [diagnosisQuery, setDiagnosisQuery] = useState2(null);
1567
+ const [diagnosisHasMore, setDiagnosisHasMore] = useState2(true);
1529
1568
  const [postalStatus, setPostalStatus] = useState2(null);
1530
1569
  const [dragActive, setDragActive] = useState2(false);
1570
+ const formRef = useRef2(null);
1531
1571
  const fileInput = useRef2(null);
1572
+ const diagnosisRequest = useRef2(0);
1573
+ const diagnosisAppendPending = useRef2(false);
1532
1574
  const procedures = useMemo2(() => mergeOptions(DEFAULT_BILL_SUBMISSION_PROCEDURES, procedureOptions), [procedureOptions]);
1533
1575
  const modifiers = useMemo2(() => mergeOptions(DEFAULT_BILL_SUBMISSION_MODIFIERS, modifierOptions), [modifierOptions]);
1534
1576
  const [evaluationType, setEvaluationType] = useState2(() => initialBill.renderingProvider?.isAme ? "ame" : initialBill.renderingProvider?.specialty?.toLowerCase().includes("psych") ? "psych_qme" : "qme");
@@ -1536,12 +1578,21 @@ function BillSubmissionForm({
1536
1578
  const locked = disabled || submitting;
1537
1579
  useEffect2(() => {
1538
1580
  setBill(cloneBill(initialBill));
1539
- setSelectedIds(attachments.filter((item) => item.selected !== false || item.autoAttached || item.documentType === "w9").map((item) => item.id));
1581
+ setSelectedIds(attachments.map((item) => item.id));
1540
1582
  setRemovedSourceIds([]);
1541
1583
  setUploads([]);
1542
1584
  setErrors({});
1585
+ setValidationActive(false);
1543
1586
  setFormError(null);
1587
+ setDiagnosisResults([]);
1588
+ setDiagnosisQuery(null);
1589
+ setDiagnosisHasMore(true);
1590
+ diagnosisRequest.current += 1;
1591
+ diagnosisAppendPending.current = false;
1544
1592
  }, [initialBill, attachments]);
1593
+ useEffect2(() => {
1594
+ if (validationActive) setErrors(validateBillSubmission(bill).fieldErrors);
1595
+ }, [bill, validationActive]);
1545
1596
  const addFiles = useCallback((fileList) => {
1546
1597
  const files = Array.from(fileList);
1547
1598
  if (!files.length) return;
@@ -1605,13 +1656,43 @@ function BillSubmissionForm({
1605
1656
  }
1606
1657
  void search(query, bill.claim.claimNumber).then(setPayerResults).catch((caught) => setFormError(caught instanceof Error ? caught.message : "Claims administrator search is unavailable.")).finally(() => setPayerLoading(false));
1607
1658
  };
1608
- const searchDiagnoses = (query) => {
1609
- if (query.trim().length < 2) return;
1659
+ const loadDiagnoses = (query, append = false) => {
1660
+ const trimmed = query.trim();
1610
1661
  const search = onSearchDiagnoses ?? referenceClient?.searchDiagnosisCodes;
1611
- if (!search) return;
1612
- setDiagnosisLoading(true);
1613
- void search(query).then(setDiagnosisResults).catch(() => setDiagnosisResults([])).finally(() => setDiagnosisLoading(false));
1662
+ if (!search) {
1663
+ setDiagnosisResults(mergeDiagnosisOptions(diagnosisOptions));
1664
+ setDiagnosisHasMore(false);
1665
+ setDiagnosisQuery(trimmed);
1666
+ return;
1667
+ }
1668
+ const continuing = append && diagnosisQuery === trimmed;
1669
+ if (continuing && (!diagnosisHasMore || diagnosisAppendPending.current)) return;
1670
+ const offset = continuing ? diagnosisResults.length : 0;
1671
+ const request = ++diagnosisRequest.current;
1672
+ if (continuing) {
1673
+ diagnosisAppendPending.current = true;
1674
+ setDiagnosisLoadingMore(true);
1675
+ } else {
1676
+ setDiagnosisLoading(true);
1677
+ setDiagnosisLoadingMore(false);
1678
+ setDiagnosisQuery(trimmed);
1679
+ setDiagnosisHasMore(true);
1680
+ }
1681
+ void search(trimmed, DIAGNOSIS_PAGE_SIZE, offset).then((page) => {
1682
+ if (request !== diagnosisRequest.current) return;
1683
+ setDiagnosisResults((current) => mergeDiagnosisOptions(continuing ? [...current, ...page] : page));
1684
+ setDiagnosisHasMore(page.length >= DIAGNOSIS_PAGE_SIZE);
1685
+ }).catch(() => {
1686
+ if (request === diagnosisRequest.current && !continuing) setDiagnosisResults([]);
1687
+ }).finally(() => {
1688
+ if (request !== diagnosisRequest.current) return;
1689
+ diagnosisAppendPending.current = false;
1690
+ setDiagnosisLoading(false);
1691
+ setDiagnosisLoadingMore(false);
1692
+ });
1614
1693
  };
1694
+ const searchDiagnoses = (query) => loadDiagnoses(query);
1695
+ const diagnosisChoices = useMemo2(() => mergeDiagnosisOptions([...diagnosisOptions, ...diagnosisResults]), [diagnosisOptions, diagnosisResults]);
1615
1696
  const updatePostalCode = (postalCode) => {
1616
1697
  setAddress({ postalCode });
1617
1698
  setPostalStatus(null);
@@ -1636,9 +1717,18 @@ function BillSubmissionForm({
1636
1717
  return charge == null ? line : { ...line, charge };
1637
1718
  }) };
1638
1719
  const validation = validateBillSubmission(clean);
1720
+ setValidationActive(true);
1639
1721
  setErrors(validation.fieldErrors);
1640
1722
  setFormError(null);
1641
- if (!validation.valid) return setFormError("Complete the required fields before submitting.");
1723
+ if (!validation.valid) {
1724
+ const count = Object.keys(validation.fieldErrors).length;
1725
+ const first = Object.values(validation.fieldErrors)[0];
1726
+ setFormError(`Fix ${count} highlighted field${count === 1 ? "" : "s"} before submitting. ${first}`);
1727
+ window.requestAnimationFrame(() => {
1728
+ if (formRef.current) focusFirstInvalid(formRef.current);
1729
+ });
1730
+ return;
1731
+ }
1642
1732
  if (selectedIds.length + uploads.length > MAX_DOCUMENTS) return setFormError(`A bill can include at most ${MAX_DOCUMENTS} attachments.`);
1643
1733
  setSubmitting(true);
1644
1734
  try {
@@ -1649,7 +1739,8 @@ function BillSubmissionForm({
1649
1739
  setSubmitting(false);
1650
1740
  }
1651
1741
  }
1652
- return /* @__PURE__ */ jsxs3("form", { className: `${className} mbsf`, style: { ...mindBillAppearanceStyle(appearance), ...style }, onSubmit: (event) => {
1742
+ const claimsAdministratorError = errors["claim.claimsAdministrator"] ?? (!bill.claim.claimsAdministrator?.id ? "Required for routing \u2014 search and select a claims administrator." : void 0);
1743
+ return /* @__PURE__ */ jsxs3("form", { ref: formRef, className: `${className} mbsf`, style: { ...mindBillAppearanceStyle(appearance), ...style }, onSubmit: (event) => {
1653
1744
  event.preventDefault();
1654
1745
  void submit();
1655
1746
  }, noValidate: true, children: [
@@ -1667,29 +1758,29 @@ function BillSubmissionForm({
1667
1758
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1668
1759
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Patient" }),
1669
1760
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-grid", children: [
1670
- /* @__PURE__ */ jsx3(Field2, { label: "First name", required: true, error: errors["patient.firstName"], children: text(bill.patient.firstName, (firstName) => setBill((c) => ({ ...c, patient: { ...c.patient, firstName } }))) }),
1671
- /* @__PURE__ */ jsx3(Field2, { label: "Last name", required: true, error: errors["patient.lastName"], children: text(bill.patient.lastName, (lastName) => setBill((c) => ({ ...c, patient: { ...c.patient, lastName } }))) }),
1761
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.firstName", label: "First name", required: true, error: errors["patient.firstName"], children: text(bill.patient.firstName, (firstName) => setBill((c) => ({ ...c, patient: { ...c.patient, firstName } }))) }),
1762
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.lastName", label: "Last name", required: true, error: errors["patient.lastName"], children: text(bill.patient.lastName, (lastName) => setBill((c) => ({ ...c, patient: { ...c.patient, lastName } }))) }),
1672
1763
  /* @__PURE__ */ jsx3(Field2, { label: "Middle name", children: text(bill.patient.middleName, (middleName) => setBill((c) => ({ ...c, patient: { ...c.patient, middleName } }))) }),
1673
1764
  /* @__PURE__ */ jsx3(Field2, { label: "Phone (optional)", children: text(bill.patient.phone, (phone) => setBill((c) => ({ ...c, patient: { ...c.patient, phone } })), { type: "tel" }) }),
1674
- /* @__PURE__ */ jsx3(Field2, { label: "Date of birth", required: true, error: errors["patient.dateOfBirth"], children: /* @__PURE__ */ jsx3(TextDateInput, { ariaLabel: "Date of birth", value: bill.patient.dateOfBirth, disabled: locked, required: true, onChange: (dateOfBirth) => setBill((c) => ({ ...c, patient: { ...c.patient, dateOfBirth } })) }) }),
1675
- /* @__PURE__ */ jsx3(Field2, { label: "Address", required: true, span: true, error: errors["patient.address.line1"], children: text(bill.patient.address.line1, (line1) => setAddress({ line1 }), { placeholder: "Street address" }) }),
1676
- /* @__PURE__ */ jsxs3(Field2, { label: "ZIP", required: true, error: errors["patient.address.postalCode"], children: [
1765
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.dateOfBirth", label: "Date of birth", required: true, error: errors["patient.dateOfBirth"], children: /* @__PURE__ */ jsx3(TextDateInput, { ariaLabel: "Date of birth", value: bill.patient.dateOfBirth, disabled: locked, required: true, onChange: (dateOfBirth) => setBill((c) => ({ ...c, patient: { ...c.patient, dateOfBirth } })) }) }),
1766
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.address.line1", label: "Address", required: true, span: true, error: errors["patient.address.line1"], children: text(bill.patient.address.line1, (line1) => setAddress({ line1 }), { placeholder: "Street address" }) }),
1767
+ /* @__PURE__ */ jsxs3(Field2, { path: "patient.address.postalCode", label: "ZIP", required: true, error: errors["patient.address.postalCode"], children: [
1677
1768
  text(bill.patient.address.postalCode, updatePostalCode, { maxLength: 10 }),
1678
1769
  postalStatus ? /* @__PURE__ */ jsx3("small", { className: "mbsf-help", children: postalStatus }) : null
1679
1770
  ] }),
1680
- /* @__PURE__ */ jsx3(Field2, { label: "City", required: true, error: errors["patient.address.city"], children: text(bill.patient.address.city, (city) => setAddress({ city })) }),
1681
- /* @__PURE__ */ jsx3(Field2, { label: "State", required: true, error: errors["patient.address.state"], children: text(bill.patient.address.state, (state) => setAddress({ state: state.toUpperCase() }), { maxLength: 2 }) })
1771
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.address.city", label: "City", required: true, error: errors["patient.address.city"], children: text(bill.patient.address.city, (city) => setAddress({ city })) }),
1772
+ /* @__PURE__ */ jsx3(Field2, { path: "patient.address.state", label: "State", required: true, error: errors["patient.address.state"], children: text(bill.patient.address.state, (state) => setAddress({ state: state.toUpperCase() }), { maxLength: 2 }) })
1682
1773
  ] })
1683
1774
  ] }),
1684
1775
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1685
1776
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Injury & claim" }),
1686
1777
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-grid", children: [
1687
- /* @__PURE__ */ jsx3(Field2, { label: "Date of service", required: true, error: errors["service.date"], children: /* @__PURE__ */ jsx3(TextDateInput, { ariaLabel: "Date of service", value: bill.service.date, disabled: locked, required: true, onChange: (date2) => setBill((c) => ({ ...c, service: { ...c.service, date: date2 } })) }) }),
1778
+ /* @__PURE__ */ jsx3(Field2, { path: "service.date", label: "Date of service", required: true, error: errors["service.date"], children: /* @__PURE__ */ jsx3(TextDateInput, { ariaLabel: "Date of service", value: bill.service.date, disabled: locked, required: true, onChange: (date2) => setBill((c) => ({ ...c, service: { ...c.service, date: date2 } })) }) }),
1688
1779
  /* @__PURE__ */ jsx3(Field2, { label: "Date of injury", children: /* @__PURE__ */ jsx3(TextDateInput, { ariaLabel: "Date of injury", value: bill.claim.dateOfInjury, disabled: locked, onChange: (dateOfInjury) => setBill((c) => ({ ...c, claim: { ...c.claim, dateOfInjury } })) }) }),
1689
1780
  /* @__PURE__ */ jsx3(Field2, { label: "Employer (optional)", children: text(bill.claim.employer, (employer) => setBill((c) => ({ ...c, claim: { ...c.claim, employer } }))) }),
1690
- /* @__PURE__ */ jsx3(Field2, { label: "Claim number", required: true, error: errors["claim.claimNumber"], children: text(bill.claim.claimNumber, (claimNumber) => setBill((c) => ({ ...c, claim: { ...c.claim, claimNumber } }))) }),
1781
+ /* @__PURE__ */ jsx3(Field2, { path: "claim.claimNumber", label: "Claim number", required: true, error: errors["claim.claimNumber"], children: text(bill.claim.claimNumber, (claimNumber) => setBill((c) => ({ ...c, claim: { ...c.claim, claimNumber } }))) }),
1691
1782
  /* @__PURE__ */ jsx3(Field2, { label: "WCAB / ADJ number (optional)", children: text(bill.claim.adjNumber, (adjNumber) => setBill((c) => ({ ...c, claim: { ...c.claim, adjNumber } }))) }),
1692
- /* @__PURE__ */ jsx3(Field2, { label: "Claims administrator", required: true, span: true, error: errors["claim.claimsAdministrator"], children: /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: "Claims administrator", disabled: locked, loading: payerLoading, value: bill.claim.claimsAdministrator?.name ?? "", placeholder: "Search the payer directory\u2026", options: payerResults.map((payer) => ({ id: payer.id, label: payer.name, detail: [payer.hasElectronic ? "Electronic" : "Work comp", ...payer.states ?? []].join(" \xB7 ") })), onQuery: searchPayers, onSelect: (option) => setBill((c) => ({ ...c, claim: { ...c.claim, claimsAdministrator: { id: option.id, name: option.label } } })) }) }),
1783
+ /* @__PURE__ */ jsx3(Field2, { path: "claim.claimsAdministrator", label: "Claims administrator", required: true, span: true, error: claimsAdministratorError, children: /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: "Claims administrator", invalid: Boolean(claimsAdministratorError), disabled: locked, loading: payerLoading, value: bill.claim.claimsAdministrator?.name ?? "", placeholder: "Search the payer directory\u2026", options: payerResults.map((payer) => ({ id: payer.id, label: payer.name, detail: [payer.hasElectronic ? "Electronic" : "Work comp", ...payer.states ?? []].join(" \xB7 ") })), onQuery: searchPayers, onSelect: (option) => setBill((c) => ({ ...c, claim: { ...c.claim, claimsAdministrator: { id: option.id, name: option.label } } })) }) }),
1693
1784
  /* @__PURE__ */ jsx3(Field2, { label: "Injury description (optional)", span: true, children: text(bill.claim.description, (description2) => setBill((c) => ({ ...c, claim: { ...c.claim, description: description2 } }))) }),
1694
1785
  /* @__PURE__ */ jsxs3(Field2, { label: "Diagnosis codes (ICD-10)", span: true, children: [
1695
1786
  /* @__PURE__ */ jsx3("div", { className: "mbsf-quick-picks", "aria-label": "Common diagnosis codes", children: BILL_SUBMISSION_DIAGNOSIS_QUICK_PICKS.map((option) => {
@@ -1701,14 +1792,16 @@ function BillSubmissionForm({
1701
1792
  ] }, option.code);
1702
1793
  }) }),
1703
1794
  /* @__PURE__ */ jsx3("div", { className: "mbsf-chips", children: (bill.diagnoses ?? []).map((code) => {
1704
- const option = [...BILL_SUBMISSION_DIAGNOSIS_QUICK_PICKS, ...diagnosisOptions, ...diagnosisResults].find((item) => item.code === code);
1795
+ const option = [...BILL_SUBMISSION_DIAGNOSIS_QUICK_PICKS, ...diagnosisChoices].find((item) => item.code === code);
1705
1796
  return /* @__PURE__ */ jsxs3("span", { className: "mbsf-chip", children: [
1706
1797
  /* @__PURE__ */ jsx3("strong", { children: code }),
1707
1798
  option?.description ? ` ${option.description}` : "",
1708
1799
  /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `Remove ${code}`, onClick: () => setBill((c) => ({ ...c, diagnoses: (c.diagnoses ?? []).filter((item) => item !== code) })), children: "\xD7" })
1709
1800
  ] }, code);
1710
1801
  }) }),
1711
- /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: "Add diagnosis code", disabled: locked, loading: diagnosisLoading, value: "", placeholder: bill.diagnoses?.length ?? 0 ? `${bill.diagnoses.length} selected \u2014 add more\u2026` : "Search ICD-10 codes\u2026", options: [...new Map([...BILL_SUBMISSION_DIAGNOSIS_QUICK_PICKS, ...diagnosisOptions, ...diagnosisResults].map((item) => [item.code, item])).values()].filter((item) => !(bill.diagnoses ?? []).includes(item.code)).map((item) => ({ id: item.code, label: item.code, detail: item.description })), onQuery: searchDiagnoses, onSelect: (option) => setBill((c) => ({ ...c, diagnoses: [.../* @__PURE__ */ new Set([...c.diagnoses ?? [], option.id])] })) })
1802
+ /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: "Add diagnosis code", disabled: locked, loading: diagnosisLoading, loadingMore: diagnosisLoadingMore, value: "", placeholder: bill.diagnoses?.length ?? 0 ? `${bill.diagnoses.length} selected \u2014 add more\u2026` : "Search ICD-10 codes\u2026", options: diagnosisChoices.filter((item) => !(bill.diagnoses ?? []).includes(item.code)).map((item) => ({ id: item.code, label: item.code, detail: item.description })), onOpen: () => {
1803
+ if (diagnosisQuery !== "") loadDiagnoses("");
1804
+ }, onQuery: searchDiagnoses, onEndReached: () => loadDiagnoses(diagnosisQuery ?? "", true), onSelect: (option) => setBill((c) => ({ ...c, diagnoses: [.../* @__PURE__ */ new Set([...c.diagnoses ?? [], option.id])] })) })
1712
1805
  ] })
1713
1806
  ] })
1714
1807
  ] }),
@@ -1733,7 +1826,7 @@ function BillSubmissionForm({
1733
1826
  /* @__PURE__ */ jsx3("p", { className: "mbsf-help", children: "Sets the evaluator/specialty modifier on medical-legal evaluation lines." }),
1734
1827
  /* @__PURE__ */ jsx3("div", { className: "mbsf-segments", role: "group", "aria-label": "Evaluation type", children: [["qme", "QME (default)"], ["ame", "AME"], ["psych_qme", "Psych QME"]].map(([type, label]) => /* @__PURE__ */ jsx3("button", { className: "mbsf-segment", type: "button", "aria-pressed": evaluationType === type, onClick: () => changeEvaluation(type), children: label }, type)) }),
1735
1828
  /* @__PURE__ */ jsx3("p", { className: "mbsf-help", children: evaluationType === "ame" ? "Agreed Medical Evaluator \u2014 eligible ML evaluation codes default to modifier -94." : evaluationType === "psych_qme" ? "Psychiatric QME \u2014 eligible ML evaluation codes default to modifier -96 (-95 for ML200)." : "Qualified Medical Evaluator \u2014 eligible ML evaluation codes default to modifier -95." }),
1736
- /* @__PURE__ */ jsxs3("div", { className: "mbsf-lines", children: [
1829
+ /* @__PURE__ */ jsxs3("div", { className: "mbsf-lines", "data-field-path": "serviceLines", "data-invalid": Boolean(errors.serviceLines), children: [
1737
1830
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-line-head", children: [
1738
1831
  /* @__PURE__ */ jsx3("span", { children: "Procedure code" }),
1739
1832
  /* @__PURE__ */ jsx3("span", { children: "Modifiers" }),
@@ -1743,13 +1836,13 @@ function BillSubmissionForm({
1743
1836
  " "
1744
1837
  ] }),
1745
1838
  bill.serviceLines.map((line, index) => /* @__PURE__ */ jsxs3("div", { className: "mbsf-line", children: [
1746
- /* @__PURE__ */ jsxs3("div", { "data-label": "Procedure code", children: [
1747
- /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: `Procedure code ${index + 1}`, disabled: locked, value: line.code, placeholder: "Search or enter code\u2026", options: procedures.map((item) => ({ id: item.code, label: item.code, detail: item.description })), createOption: customProcedureOption, onSelect: (option) => {
1839
+ /* @__PURE__ */ jsxs3("div", { "data-label": "Procedure code", "data-field-path": `serviceLines.${index}.code`, "data-invalid": Boolean(errors[`serviceLines.${index}.code`]), children: [
1840
+ /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: `Procedure code ${index + 1}`, invalid: Boolean(errors[`serviceLines.${index}.code`]), disabled: locked, value: line.code, placeholder: "Search or enter code\u2026", options: procedures.map((item) => ({ id: item.code, label: item.code, detail: item.description })), createOption: customProcedureOption, onSelect: (option) => {
1748
1841
  const auto = evaluationModifier(evaluationType, option.id);
1749
1842
  setLine(index, { code: option.id, ...auto ? { modifiers: [auto, ...(line.modifiers ?? []).filter((item) => !["94", "95", "96"].includes(item.replace(/^-/, "")))] } : line.modifiers ? { modifiers: line.modifiers } : {} });
1750
1843
  } }),
1751
1844
  line.code ? /* @__PURE__ */ jsx3("small", { className: "mbsf-help", children: procedures.find((item) => item.code === line.code)?.description ?? "Custom CPT, HCPCS, or medical-legal code" }) : null,
1752
- errors[`serviceLines.${index}.code`] ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: errors[`serviceLines.${index}.code`] }) : null
1845
+ errors[`serviceLines.${index}.code`] ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", role: "alert", children: errors[`serviceLines.${index}.code`] }) : null
1753
1846
  ] }),
1754
1847
  /* @__PURE__ */ jsxs3("div", { "data-label": "Modifiers", children: [
1755
1848
  /* @__PURE__ */ jsx3("div", { className: "mbsf-chips", children: (line.modifiers ?? []).map((modifier) => /* @__PURE__ */ jsxs3("span", { className: "mbsf-chip", children: [
@@ -1759,9 +1852,9 @@ function BillSubmissionForm({
1759
1852
  ] }, modifier)) }),
1760
1853
  /* @__PURE__ */ jsx3(ComboBox, { ariaLabel: `Modifiers ${index + 1}`, disabled: locked, value: "", placeholder: line.modifiers?.length ?? 0 ? `${line.modifiers.length} modifier${line.modifiers.length === 1 ? "" : "s"}` : "Add modifiers\u2026", options: modifiers.filter((item) => !(line.modifiers ?? []).includes(item.code)).map((item) => ({ id: item.code, label: `\u2212${item.code}`, detail: item.description })), onSelect: (option) => setLine(index, { modifiers: [.../* @__PURE__ */ new Set([...line.modifiers ?? [], option.id])] }) })
1761
1854
  ] }),
1762
- /* @__PURE__ */ jsxs3("div", { "data-label": "Units", children: [
1763
- /* @__PURE__ */ jsx3("input", { className: "mbsf-input", "aria-label": `Units ${index + 1}`, type: "number", min: 1, value: line.units ?? 1, onChange: (event) => setLine(index, { units: Number(event.target.value) }) }),
1764
- errors[`serviceLines.${index}.units`] ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: errors[`serviceLines.${index}.units`] }) : null
1855
+ /* @__PURE__ */ jsxs3("div", { "data-label": "Units", "data-field-path": `serviceLines.${index}.units`, "data-invalid": Boolean(errors[`serviceLines.${index}.units`]), children: [
1856
+ /* @__PURE__ */ jsx3("input", { className: "mbsf-input", "aria-label": `Units ${index + 1}`, "aria-invalid": Boolean(errors[`serviceLines.${index}.units`]), type: "number", min: 1, value: line.units ?? 1, onChange: (event) => setLine(index, { units: Number(event.target.value) }) }),
1857
+ errors[`serviceLines.${index}.units`] ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", role: "alert", children: errors[`serviceLines.${index}.units`] }) : null
1765
1858
  ] }),
1766
1859
  /* @__PURE__ */ jsx3("div", { className: "mbsf-money", "data-label": "Allowed", children: lineCharge(line) == null ? "\u2014" : lineCharge(line).toLocaleString(void 0, { style: "currency", currency: "USD" }) }),
1767
1860
  /* @__PURE__ */ jsx3("button", { className: "mbsf-icon-btn", type: "button", "aria-label": `Remove service line ${index + 1}`, disabled: locked || !lineHasContent(line) && index === bill.serviceLines.length - 1, onClick: () => setBill((c) => ({ ...c, serviceLines: ensureTrailingBillSubmissionLine(c.serviceLines.filter((_, itemIndex) => itemIndex !== index)) })), children: "\xD7" })
@@ -1771,41 +1864,44 @@ function BillSubmissionForm({
1771
1864
  /* @__PURE__ */ jsx3("span", { children: total.toLocaleString(void 0, { style: "currency", currency: "USD" }) })
1772
1865
  ] })
1773
1866
  ] }),
1774
- errors.serviceLines ? /* @__PURE__ */ jsx3("p", { className: "mbsf-error", children: errors.serviceLines }) : null
1867
+ errors.serviceLines ? /* @__PURE__ */ jsx3("p", { className: "mbsf-error", role: "alert", children: errors.serviceLines }) : null
1775
1868
  ] }),
1776
1869
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1777
1870
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Attachments" }),
1778
1871
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-list", children: [
1779
1872
  attachments.filter((attachment) => !removedSourceIds.includes(attachment.id)).map((attachment) => {
1780
1873
  const auto = attachment.autoAttached || attachment.documentType === "w9";
1781
- const selected = selectedIds.includes(attachment.id);
1782
1874
  const removable = !auto && attachment.removable !== false;
1783
1875
  return /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-row", "data-auto": auto, children: [
1784
- /* @__PURE__ */ jsxs3("label", { className: "mbsf-attach-row", style: { border: 0, padding: 0, flex: 1, justifyContent: "flex-start" }, children: [
1785
- auto ? /* @__PURE__ */ jsx3("span", { "aria-label": "Always attached", role: "img", children: "\u2713" }) : /* @__PURE__ */ jsx3("input", { type: "checkbox", checked: selected, disabled: locked, onChange: (event) => setSelectedIds((current) => event.target.checked ? [.../* @__PURE__ */ new Set([...current, attachment.id])] : current.filter((id) => id !== attachment.id)) }),
1876
+ /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-main", children: [
1877
+ auto ? /* @__PURE__ */ jsx3("span", { "aria-label": "Always attached", role: "img", children: "\u2713" }) : null,
1786
1878
  /* @__PURE__ */ jsxs3("span", { className: "mbsf-file", children: [
1787
1879
  /* @__PURE__ */ jsx3("strong", { children: attachment.fileName }),
1788
1880
  /* @__PURE__ */ jsx3("span", { className: "mbsf-badge", children: auto ? "Auto-attached" : documentLabels[attachment.documentType] }),
1789
1881
  /* @__PURE__ */ jsx3("span", { className: "mbsf-help", style: { display: "block" }, children: attachment.description || (auto ? "Included automatically with every bill." : documentLabels[attachment.documentType]) })
1790
1882
  ] })
1791
1883
  ] }),
1792
- attachment.previewUrl ? /* @__PURE__ */ jsx3("a", { className: "mbsf-secondary", href: attachment.previewUrl, target: "_blank", rel: "noreferrer", children: "View" }) : null,
1793
- removable ? /* @__PURE__ */ jsx3("button", { className: "mbsf-icon-btn", type: "button", "aria-label": `Remove ${attachment.fileName}`, disabled: locked, onClick: () => {
1794
- setSelectedIds((current) => current.filter((id) => id !== attachment.id));
1795
- setRemovedSourceIds((current) => [.../* @__PURE__ */ new Set([...current, attachment.id])]);
1796
- }, children: "\xD7" }) : null
1884
+ /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-actions", children: [
1885
+ attachment.previewUrl ? /* @__PURE__ */ jsx3("a", { className: "mbsf-secondary", href: attachment.previewUrl, target: "_blank", rel: "noopener noreferrer", children: "Preview" }) : null,
1886
+ removable ? /* @__PURE__ */ jsx3("button", { className: "mbsf-icon-btn", type: "button", "aria-label": `Remove ${attachment.fileName}`, disabled: locked, onClick: () => {
1887
+ setSelectedIds((current) => current.filter((id) => id !== attachment.id));
1888
+ setRemovedSourceIds((current) => [.../* @__PURE__ */ new Set([...current, attachment.id])]);
1889
+ }, children: "\xD7" }) : null
1890
+ ] })
1797
1891
  ] }, attachment.id);
1798
1892
  }),
1799
1893
  uploads.map((upload, index) => /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-row", children: [
1800
- /* @__PURE__ */ jsxs3("span", { className: "mbsf-file", children: [
1894
+ /* @__PURE__ */ jsx3("div", { className: "mbsf-attach-main", children: /* @__PURE__ */ jsxs3("span", { className: "mbsf-file", children: [
1801
1895
  /* @__PURE__ */ jsx3("strong", { children: upload.file.name }),
1802
1896
  /* @__PURE__ */ jsxs3("span", { className: "mbsf-help", style: { display: "block" }, children: [
1803
1897
  (upload.file.size / 1024 / 1024).toFixed(1),
1804
1898
  " MB"
1805
1899
  ] })
1806
- ] }),
1807
- /* @__PURE__ */ jsx3("select", { className: "mbsf-select", style: { width: 190 }, value: upload.documentType, onChange: (event) => setUploads((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, documentType: event.target.value } : item)), children: BILL_SUBMISSION_DOCUMENT_TYPES.map((type) => /* @__PURE__ */ jsx3("option", { value: type, children: documentLabels[type] }, type)) }),
1808
- /* @__PURE__ */ jsx3("button", { className: "mbsf-icon-btn", type: "button", "aria-label": `Remove ${upload.file.name}`, onClick: () => setUploads((current) => current.filter((_, itemIndex) => itemIndex !== index)), children: "\xD7" })
1900
+ ] }) }),
1901
+ /* @__PURE__ */ jsxs3("div", { className: "mbsf-attach-actions", children: [
1902
+ /* @__PURE__ */ jsx3("button", { className: "mbsf-secondary", type: "button", onClick: () => previewUploadedPdf(upload.file), children: "Preview" }),
1903
+ /* @__PURE__ */ jsx3("button", { className: "mbsf-icon-btn", type: "button", "aria-label": `Remove ${upload.file.name}`, onClick: () => setUploads((current) => current.filter((_, itemIndex) => itemIndex !== index)), children: "\xD7" })
1904
+ ] })
1809
1905
  ] }, `${upload.file.name}-${index}`))
1810
1906
  ] }),
1811
1907
  /* @__PURE__ */ jsx3("input", { ref: fileInput, hidden: true, type: "file", accept: "application/pdf,.pdf", multiple: true, onChange: (event) => {