@mindbill/react 0.21.1 → 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,7 +153,7 @@ 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;
@@ -161,6 +161,8 @@ The component includes the interaction model, not just the markup:
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
@@ -481,7 +481,7 @@ type BillSubmissionFormProps = {
481
481
  fetch?: typeof globalThis.fetch;
482
482
  onSearchClaimsAdministrators?: (query: string, claimNumber?: string) => Promise<BillReviewPayer$1[]>;
483
483
  diagnosisOptions?: BillSubmissionDiagnosisOption[];
484
- onSearchDiagnoses?: (query: string) => Promise<BillSubmissionDiagnosisOption[]>;
484
+ onSearchDiagnoses?: (query: string, limit?: number, offset?: number) => Promise<BillSubmissionDiagnosisOption[]>;
485
485
  onLookupPostalCode?: (postalCode: string) => Promise<BillSubmissionPostalPlace | null>;
486
486
  procedureOptions?: BillSubmissionProcedureOption[];
487
487
  modifierOptions?: BillSubmissionModifierOption[];
package/dist/index.js CHANGED
@@ -1320,6 +1320,7 @@ 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 = [];
1324
1325
  function previewUploadedPdf(file) {
1325
1326
  const url = URL.createObjectURL(file);
@@ -1389,19 +1390,19 @@ function cloneBill(bill) {
1389
1390
  }
1390
1391
  function validateBillSubmission(bill) {
1391
1392
  const errors = {};
1392
- const required = (path, value) => {
1393
- 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;
1394
1395
  };
1395
- required("patient.firstName", bill.patient.firstName);
1396
- required("patient.lastName", bill.patient.lastName);
1397
- required("patient.dateOfBirth", bill.patient.dateOfBirth);
1398
- required("patient.address.line1", bill.patient.address.line1);
1399
- required("patient.address.city", bill.patient.address.city);
1400
- required("patient.address.state", bill.patient.address.state);
1401
- required("patient.address.postalCode", bill.patient.address.postalCode);
1402
- required("claim.claimNumber", bill.claim.claimNumber);
1403
- required("claim.claimsAdministrator", bill.claim.claimsAdministrator?.name);
1404
- 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.");
1405
1406
  if (bill.claim.claimsAdministrator?.name && !bill.claim.claimsAdministrator.id) {
1406
1407
  errors["claim.claimsAdministrator"] = "Select a claims administrator from the payer directory.";
1407
1408
  }
@@ -1410,7 +1411,7 @@ function validateBillSubmission(bill) {
1410
1411
  const lines = submittedLines(bill.serviceLines);
1411
1412
  if (!lines.length) errors.serviceLines = "Add at least one service line";
1412
1413
  lines.forEach((line, index) => {
1413
- required(`serviceLines.${index}.code`, line.code);
1414
+ required(`serviceLines.${index}.code`, line.code, "Select a procedure code.");
1414
1415
  if (!Number.isInteger(line.units) || (line.units ?? 0) < 1) errors[`serviceLines.${index}.units`] = "Enter at least 1 unit";
1415
1416
  if (bill.billingMode === "professional" && (!Number.isFinite(line.charge) || (line.charge ?? 0) < 0)) errors[`serviceLines.${index}.charge`] = "Enter the billed charge";
1416
1417
  });
@@ -1419,25 +1420,25 @@ function validateBillSubmission(bill) {
1419
1420
  var css = `
1420
1421
  .mbsf{display:grid;gap:20px;color:var(--mb-text);font-family:var(--mb-font);font-size:15px}.mbsf *{box-sizing:border-box}
1421
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)}
1422
- .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)}
1423
- .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}
1424
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)}
1425
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)}
1426
- .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}
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}
1427
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}
1428
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%}}
1429
1430
  `;
1430
1431
  function RequiredMark() {
1431
1432
  return /* @__PURE__ */ jsx3("span", { className: "mbsf-star", children: " *" });
1432
1433
  }
1433
- function Field2({ label, required, error, span, children }) {
1434
- 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: [
1435
1436
  /* @__PURE__ */ jsxs3("span", { className: "mbsf-label", children: [
1436
1437
  label,
1437
1438
  required ? /* @__PURE__ */ jsx3(RequiredMark, {}) : null
1438
1439
  ] }),
1439
1440
  children,
1440
- error ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: error }) : null
1441
+ error ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", role: "alert", children: error }) : null
1441
1442
  ] });
1442
1443
  }
1443
1444
  function TextDateInput({ value, onChange, disabled, required, ariaLabel }) {
@@ -1459,7 +1460,7 @@ function TextDateInput({ value, onChange, disabled, required, ariaLabel }) {
1459
1460
  invalid ? /* @__PURE__ */ jsx3("small", { className: "mbsf-error", children: "Use MM/DD/YYYY" }) : null
1460
1461
  ] });
1461
1462
  }
1462
- 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 }) {
1463
1464
  const [open, setOpen] = useState2(false);
1464
1465
  const [query, setQuery] = useState2("");
1465
1466
  const visible = useMemo2(() => {
@@ -1470,24 +1471,50 @@ function ComboBox({ value, placeholder, options, disabled, loading, onQuery, onS
1470
1471
  }, [createOption, options, query]);
1471
1472
  const showMenu = open && (loading || visible.length > 0 || query.trim().length >= 2);
1472
1473
  return /* @__PURE__ */ jsxs3("div", { className: "mbsf-combo", children: [
1473
- /* @__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: () => {
1474
1475
  setOpen(true);
1475
1476
  setQuery("");
1477
+ onOpen?.();
1476
1478
  }, onChange: (event) => {
1477
1479
  setOpen(true);
1478
1480
  setQuery(event.target.value);
1479
1481
  onQuery?.(event.target.value);
1480
1482
  }, onBlur: () => setTimeout(() => setOpen(false), 120) }),
1481
- 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: () => {
1482
- onSelect(option);
1483
- setOpen(false);
1484
- 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?.();
1485
1486
  }, children: [
1486
- /* @__PURE__ */ jsx3("strong", { children: option.label }),
1487
- option.detail ? /* @__PURE__ */ jsx3("small", { children: option.detail }) : null
1488
- ] }, 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
1489
1497
  ] });
1490
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
+ }
1491
1518
  function customProcedureOption(query) {
1492
1519
  const code = query.trim().toUpperCase().replace(/\s+/g, "");
1493
1520
  if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code)) return null;
@@ -1498,6 +1525,9 @@ function mergeOptions(defaults, supplied) {
1498
1525
  supplied?.forEach((item) => map.set(item.code.toUpperCase(), item));
1499
1526
  return [...map.values()];
1500
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
+ }
1501
1531
  function BillSubmissionForm({
1502
1532
  initialBill,
1503
1533
  attachments = EMPTY_ATTACHMENTS,
@@ -1525,15 +1555,22 @@ function BillSubmissionForm({
1525
1555
  const [removedSourceIds, setRemovedSourceIds] = useState2([]);
1526
1556
  const [uploads, setUploads] = useState2([]);
1527
1557
  const [errors, setErrors] = useState2({});
1558
+ const [validationActive, setValidationActive] = useState2(false);
1528
1559
  const [formError, setFormError] = useState2(null);
1529
1560
  const [submitting, setSubmitting] = useState2(false);
1530
1561
  const [payerResults, setPayerResults] = useState2([]);
1531
1562
  const [payerLoading, setPayerLoading] = useState2(false);
1532
- const [diagnosisResults, setDiagnosisResults] = useState2(diagnosisOptions);
1563
+ const [diagnosisResults, setDiagnosisResults] = useState2([]);
1533
1564
  const [diagnosisLoading, setDiagnosisLoading] = useState2(false);
1565
+ const [diagnosisLoadingMore, setDiagnosisLoadingMore] = useState2(false);
1566
+ const [diagnosisQuery, setDiagnosisQuery] = useState2(null);
1567
+ const [diagnosisHasMore, setDiagnosisHasMore] = useState2(true);
1534
1568
  const [postalStatus, setPostalStatus] = useState2(null);
1535
1569
  const [dragActive, setDragActive] = useState2(false);
1570
+ const formRef = useRef2(null);
1536
1571
  const fileInput = useRef2(null);
1572
+ const diagnosisRequest = useRef2(0);
1573
+ const diagnosisAppendPending = useRef2(false);
1537
1574
  const procedures = useMemo2(() => mergeOptions(DEFAULT_BILL_SUBMISSION_PROCEDURES, procedureOptions), [procedureOptions]);
1538
1575
  const modifiers = useMemo2(() => mergeOptions(DEFAULT_BILL_SUBMISSION_MODIFIERS, modifierOptions), [modifierOptions]);
1539
1576
  const [evaluationType, setEvaluationType] = useState2(() => initialBill.renderingProvider?.isAme ? "ame" : initialBill.renderingProvider?.specialty?.toLowerCase().includes("psych") ? "psych_qme" : "qme");
@@ -1545,8 +1582,17 @@ function BillSubmissionForm({
1545
1582
  setRemovedSourceIds([]);
1546
1583
  setUploads([]);
1547
1584
  setErrors({});
1585
+ setValidationActive(false);
1548
1586
  setFormError(null);
1587
+ setDiagnosisResults([]);
1588
+ setDiagnosisQuery(null);
1589
+ setDiagnosisHasMore(true);
1590
+ diagnosisRequest.current += 1;
1591
+ diagnosisAppendPending.current = false;
1549
1592
  }, [initialBill, attachments]);
1593
+ useEffect2(() => {
1594
+ if (validationActive) setErrors(validateBillSubmission(bill).fieldErrors);
1595
+ }, [bill, validationActive]);
1550
1596
  const addFiles = useCallback((fileList) => {
1551
1597
  const files = Array.from(fileList);
1552
1598
  if (!files.length) return;
@@ -1610,17 +1656,43 @@ function BillSubmissionForm({
1610
1656
  }
1611
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));
1612
1658
  };
1613
- const searchDiagnoses = (query) => {
1659
+ const loadDiagnoses = (query, append = false) => {
1614
1660
  const trimmed = query.trim();
1615
- if (!trimmed) {
1616
- setDiagnosisResults(diagnosisOptions);
1661
+ const search = onSearchDiagnoses ?? referenceClient?.searchDiagnosisCodes;
1662
+ if (!search) {
1663
+ setDiagnosisResults(mergeDiagnosisOptions(diagnosisOptions));
1664
+ setDiagnosisHasMore(false);
1665
+ setDiagnosisQuery(trimmed);
1617
1666
  return;
1618
1667
  }
1619
- const search = onSearchDiagnoses ?? referenceClient?.searchDiagnosisCodes;
1620
- if (!search) return;
1621
- setDiagnosisLoading(true);
1622
- void search(trimmed).then(setDiagnosisResults).catch(() => setDiagnosisResults([])).finally(() => setDiagnosisLoading(false));
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
+ });
1623
1693
  };
1694
+ const searchDiagnoses = (query) => loadDiagnoses(query);
1695
+ const diagnosisChoices = useMemo2(() => mergeDiagnosisOptions([...diagnosisOptions, ...diagnosisResults]), [diagnosisOptions, diagnosisResults]);
1624
1696
  const updatePostalCode = (postalCode) => {
1625
1697
  setAddress({ postalCode });
1626
1698
  setPostalStatus(null);
@@ -1645,9 +1717,18 @@ function BillSubmissionForm({
1645
1717
  return charge == null ? line : { ...line, charge };
1646
1718
  }) };
1647
1719
  const validation = validateBillSubmission(clean);
1720
+ setValidationActive(true);
1648
1721
  setErrors(validation.fieldErrors);
1649
1722
  setFormError(null);
1650
- 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
+ }
1651
1732
  if (selectedIds.length + uploads.length > MAX_DOCUMENTS) return setFormError(`A bill can include at most ${MAX_DOCUMENTS} attachments.`);
1652
1733
  setSubmitting(true);
1653
1734
  try {
@@ -1658,7 +1739,8 @@ function BillSubmissionForm({
1658
1739
  setSubmitting(false);
1659
1740
  }
1660
1741
  }
1661
- 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) => {
1662
1744
  event.preventDefault();
1663
1745
  void submit();
1664
1746
  }, noValidate: true, children: [
@@ -1676,29 +1758,29 @@ function BillSubmissionForm({
1676
1758
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1677
1759
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Patient" }),
1678
1760
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-grid", children: [
1679
- /* @__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 } }))) }),
1680
- /* @__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 } }))) }),
1681
1763
  /* @__PURE__ */ jsx3(Field2, { label: "Middle name", children: text(bill.patient.middleName, (middleName) => setBill((c) => ({ ...c, patient: { ...c.patient, middleName } }))) }),
1682
1764
  /* @__PURE__ */ jsx3(Field2, { label: "Phone (optional)", children: text(bill.patient.phone, (phone) => setBill((c) => ({ ...c, patient: { ...c.patient, phone } })), { type: "tel" }) }),
1683
- /* @__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 } })) }) }),
1684
- /* @__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" }) }),
1685
- /* @__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: [
1686
1768
  text(bill.patient.address.postalCode, updatePostalCode, { maxLength: 10 }),
1687
1769
  postalStatus ? /* @__PURE__ */ jsx3("small", { className: "mbsf-help", children: postalStatus }) : null
1688
1770
  ] }),
1689
- /* @__PURE__ */ jsx3(Field2, { label: "City", required: true, error: errors["patient.address.city"], children: text(bill.patient.address.city, (city) => setAddress({ city })) }),
1690
- /* @__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 }) })
1691
1773
  ] })
1692
1774
  ] }),
1693
1775
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1694
1776
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Injury & claim" }),
1695
1777
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-grid", children: [
1696
- /* @__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 } })) }) }),
1697
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 } })) }) }),
1698
1780
  /* @__PURE__ */ jsx3(Field2, { label: "Employer (optional)", children: text(bill.claim.employer, (employer) => setBill((c) => ({ ...c, claim: { ...c.claim, employer } }))) }),
1699
- /* @__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 } }))) }),
1700
1782
  /* @__PURE__ */ jsx3(Field2, { label: "WCAB / ADJ number (optional)", children: text(bill.claim.adjNumber, (adjNumber) => setBill((c) => ({ ...c, claim: { ...c.claim, adjNumber } }))) }),
1701
- /* @__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 } } })) }) }),
1702
1784
  /* @__PURE__ */ jsx3(Field2, { label: "Injury description (optional)", span: true, children: text(bill.claim.description, (description2) => setBill((c) => ({ ...c, claim: { ...c.claim, description: description2 } }))) }),
1703
1785
  /* @__PURE__ */ jsxs3(Field2, { label: "Diagnosis codes (ICD-10)", span: true, children: [
1704
1786
  /* @__PURE__ */ jsx3("div", { className: "mbsf-quick-picks", "aria-label": "Common diagnosis codes", children: BILL_SUBMISSION_DIAGNOSIS_QUICK_PICKS.map((option) => {
@@ -1710,14 +1792,16 @@ function BillSubmissionForm({
1710
1792
  ] }, option.code);
1711
1793
  }) }),
1712
1794
  /* @__PURE__ */ jsx3("div", { className: "mbsf-chips", children: (bill.diagnoses ?? []).map((code) => {
1713
- 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);
1714
1796
  return /* @__PURE__ */ jsxs3("span", { className: "mbsf-chip", children: [
1715
1797
  /* @__PURE__ */ jsx3("strong", { children: code }),
1716
1798
  option?.description ? ` ${option.description}` : "",
1717
1799
  /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `Remove ${code}`, onClick: () => setBill((c) => ({ ...c, diagnoses: (c.diagnoses ?? []).filter((item) => item !== code) })), children: "\xD7" })
1718
1800
  ] }, code);
1719
1801
  }) }),
1720
- /* @__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])] })) })
1721
1805
  ] })
1722
1806
  ] })
1723
1807
  ] }),
@@ -1742,7 +1826,7 @@ function BillSubmissionForm({
1742
1826
  /* @__PURE__ */ jsx3("p", { className: "mbsf-help", children: "Sets the evaluator/specialty modifier on medical-legal evaluation lines." }),
1743
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)) }),
1744
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." }),
1745
- /* @__PURE__ */ jsxs3("div", { className: "mbsf-lines", children: [
1829
+ /* @__PURE__ */ jsxs3("div", { className: "mbsf-lines", "data-field-path": "serviceLines", "data-invalid": Boolean(errors.serviceLines), children: [
1746
1830
  /* @__PURE__ */ jsxs3("div", { className: "mbsf-line-head", children: [
1747
1831
  /* @__PURE__ */ jsx3("span", { children: "Procedure code" }),
1748
1832
  /* @__PURE__ */ jsx3("span", { children: "Modifiers" }),
@@ -1752,13 +1836,13 @@ function BillSubmissionForm({
1752
1836
  " "
1753
1837
  ] }),
1754
1838
  bill.serviceLines.map((line, index) => /* @__PURE__ */ jsxs3("div", { className: "mbsf-line", children: [
1755
- /* @__PURE__ */ jsxs3("div", { "data-label": "Procedure code", children: [
1756
- /* @__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) => {
1757
1841
  const auto = evaluationModifier(evaluationType, option.id);
1758
1842
  setLine(index, { code: option.id, ...auto ? { modifiers: [auto, ...(line.modifiers ?? []).filter((item) => !["94", "95", "96"].includes(item.replace(/^-/, "")))] } : line.modifiers ? { modifiers: line.modifiers } : {} });
1759
1843
  } }),
1760
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,
1761
- 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
1762
1846
  ] }),
1763
1847
  /* @__PURE__ */ jsxs3("div", { "data-label": "Modifiers", children: [
1764
1848
  /* @__PURE__ */ jsx3("div", { className: "mbsf-chips", children: (line.modifiers ?? []).map((modifier) => /* @__PURE__ */ jsxs3("span", { className: "mbsf-chip", children: [
@@ -1768,9 +1852,9 @@ function BillSubmissionForm({
1768
1852
  ] }, modifier)) }),
1769
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])] }) })
1770
1854
  ] }),
1771
- /* @__PURE__ */ jsxs3("div", { "data-label": "Units", children: [
1772
- /* @__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) }) }),
1773
- 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
1774
1858
  ] }),
1775
1859
  /* @__PURE__ */ jsx3("div", { className: "mbsf-money", "data-label": "Allowed", children: lineCharge(line) == null ? "\u2014" : lineCharge(line).toLocaleString(void 0, { style: "currency", currency: "USD" }) }),
1776
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" })
@@ -1780,7 +1864,7 @@ function BillSubmissionForm({
1780
1864
  /* @__PURE__ */ jsx3("span", { children: total.toLocaleString(void 0, { style: "currency", currency: "USD" }) })
1781
1865
  ] })
1782
1866
  ] }),
1783
- 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
1784
1868
  ] }),
1785
1869
  /* @__PURE__ */ jsxs3("fieldset", { className: "mbsf-card", disabled: locked, children: [
1786
1870
  /* @__PURE__ */ jsx3("legend", { className: "mbsf-legend", children: "Attachments" }),