@mindbill/react 0.52.0 → 0.53.0

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/dist/index.js CHANGED
@@ -348,7 +348,7 @@ function organizationProfileOptions(profile) {
348
348
 
349
349
  // src/index.tsx
350
350
  import "@mindbill/embed";
351
- import { createElement, useEffect as useEffect16, useRef as useRef11 } from "react";
351
+ import { createElement, useEffect as useEffect16, useRef as useRef12 } from "react";
352
352
 
353
353
  // src/native-bill-review.tsx
354
354
  import { sanitizeBillReviewSaveInput } from "@mindbill/browser";
@@ -2474,27 +2474,31 @@ function formatBillSubmissionDate(value) {
2474
2474
  const [year, month, day] = parsed.split("-");
2475
2475
  return `${month}/${day}/${year}`;
2476
2476
  }
2477
- function evaluationModifier(type, code) {
2478
- if (!/^ML(?:200|201|202|203|PRR)$/.test(code.toUpperCase())) return void 0;
2479
- if (type === "ame") return code.toUpperCase() === "ML200" || code.toUpperCase() === "MLPRR" ? void 0 : "94";
2480
- if (type === "psych_qme") return code.toUpperCase() === "ML200" ? "95" : code.toUpperCase() === "MLPRR" ? void 0 : "96";
2481
- return "95";
2477
+ function evaluationModifiers(type, code) {
2478
+ const normalizedCode = code.toUpperCase();
2479
+ if (!/^ML(?:200|201|202|203|PRR)$/.test(normalizedCode)) return [];
2480
+ const isAme = type === "ame" || type === "psych_ame";
2481
+ if (normalizedCode === "ML200" || normalizedCode === "MLPRR") return isAme ? [] : ["95"];
2482
+ const modifiers = [isAme ? "94" : "95"];
2483
+ if (type === "psych_qme" || type === "psych_ame") modifiers.push("96");
2484
+ return modifiers;
2482
2485
  }
2483
2486
  function applyBillSubmissionEvaluationModifiers(lines, type) {
2484
2487
  return lines.map((line) => {
2485
- const auto = evaluationModifier(type, line.code);
2488
+ const auto = evaluationModifiers(type, line.code);
2486
2489
  const modifiers = (line.modifiers ?? []).filter((item) => !["94", "95", "96"].includes(item.replace(/^-/, "")));
2487
- return { ...line, modifiers: auto ? [auto, ...modifiers] : modifiers };
2490
+ return { ...line, modifiers: [...auto, ...modifiers] };
2488
2491
  });
2489
2492
  }
2490
2493
  var PSYCH_QME_DEFAULT_DIAGNOSIS = "Z04.6";
2491
2494
  function applyBillSubmissionEvaluationDiagnoses(diagnoses, type) {
2492
2495
  const current = [...diagnoses ?? []];
2493
- return type === "psych_qme" && !current.some((code) => code.trim()) ? [PSYCH_QME_DEFAULT_DIAGNOSIS] : current;
2496
+ return (type === "psych_qme" || type === "psych_ame") && !current.some((code) => code.trim()) ? [PSYCH_QME_DEFAULT_DIAGNOSIS] : current;
2494
2497
  }
2495
2498
  function initialEvaluationType(bill) {
2496
- if (bill.renderingProvider?.isAme) return "ame";
2497
- return bill.renderingProvider?.specialty?.toLowerCase().includes("psych") ? "psych_qme" : "qme";
2499
+ const isPsych = bill.renderingProvider?.specialty?.toLowerCase().includes("psych");
2500
+ if (bill.renderingProvider?.isAme) return isPsych ? "psych_ame" : "ame";
2501
+ return isPsych ? "psych_qme" : "qme";
2498
2502
  }
2499
2503
  function cloneInitialBill(bill) {
2500
2504
  const cloned = cloneBill(bill);
@@ -3069,7 +3073,7 @@ function BillSubmissionForm({
3069
3073
  };
3070
3074
  const changeEvaluation = (type) => {
3071
3075
  setEvaluationType(type);
3072
- setBill((current) => ({ ...current, diagnoses: applyBillSubmissionEvaluationDiagnoses(current.diagnoses, type), renderingProvider: { ...current.renderingProvider, isAme: type === "ame", isQme: type !== "ame", ...type === "psych_qme" && !current.renderingProvider?.specialty ? { specialty: "Psychiatry" } : {} }, serviceLines: applyBillSubmissionEvaluationModifiers(current.serviceLines, type) }));
3076
+ setBill((current) => ({ ...current, diagnoses: applyBillSubmissionEvaluationDiagnoses(current.diagnoses, type), renderingProvider: { ...current.renderingProvider, isAme: type === "ame" || type === "psych_ame", isQme: type === "qme" || type === "psych_qme", ...(type === "psych_qme" || type === "psych_ame") && !current.renderingProvider?.specialty ? { specialty: "Psychiatry" } : {} }, serviceLines: applyBillSubmissionEvaluationModifiers(current.serviceLines, type) }));
3073
3077
  };
3074
3078
  const lineCharge = (line) => calculateBillSubmissionAllowedAmount(line, procedures) ?? (Number.isFinite(line.charge) ? Number(line.charge) : void 0);
3075
3079
  const total = submittedLines(bill.serviceLines).reduce((sum, line) => sum + (lineCharge(line) ?? 0), 0);
@@ -3393,8 +3397,8 @@ function BillSubmissionForm({
3393
3397
  const serviceLinesSection = /* @__PURE__ */ jsxs6("fieldset", { className: "mbsf-card", disabled: locked, children: [
3394
3398
  /* @__PURE__ */ jsx6("legend", { className: "mbsf-legend", children: "Evaluation & service lines" }),
3395
3399
  /* @__PURE__ */ jsx6("p", { className: "mbsf-help", children: "Sets the evaluator/specialty modifier on medical-legal evaluation lines." }),
3396
- /* @__PURE__ */ jsx6("div", { className: "mbsf-segments", role: "group", "aria-label": "Evaluation type", children: [["qme", "QME (default)"], ["ame", "AME"], ["psych_qme", "Psych QME"]].map(([type, label]) => /* @__PURE__ */ jsx6("button", { className: "mbsf-segment", type: "button", "aria-pressed": evaluationType === type, onClick: () => changeEvaluation(type), children: label }, type)) }),
3397
- /* @__PURE__ */ jsx6("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." }),
3400
+ /* @__PURE__ */ jsx6("div", { className: "mbsf-segments", role: "group", "aria-label": "Evaluation type", children: [["qme", "QME (default)"], ["ame", "AME"], ["psych_qme", "Psych QME"], ["psych_ame", "Psych AME"]].map(([type, label]) => /* @__PURE__ */ jsx6("button", { className: "mbsf-segment", type: "button", "aria-pressed": evaluationType === type, onClick: () => changeEvaluation(type), children: label }, type)) }),
3401
+ /* @__PURE__ */ jsx6("p", { className: "mbsf-help", children: evaluationType === "psych_ame" ? "Psychiatric or psychological AME \u2014 eligible ML evaluation codes default to modifiers -94 and -96." : evaluationType === "ame" ? "Agreed Medical Evaluator \u2014 eligible ML evaluation codes default to modifier -94." : evaluationType === "psych_qme" ? "Psychiatric or psychological QME \u2014 eligible ML evaluation codes default to modifiers -95 and -96 (-95 only for ML200 and MLPRR)." : "Qualified Medical Evaluator \u2014 eligible ML evaluation codes default to modifier -95." }),
3398
3402
  /* @__PURE__ */ jsxs6("div", { className: "mbsf-lines", "data-field-path": "serviceLines", "data-invalid": Boolean(errors.serviceLines), children: [
3399
3403
  /* @__PURE__ */ jsxs6("div", { className: "mbsf-line-head", children: [
3400
3404
  /* @__PURE__ */ jsxs6("span", { children: [
@@ -3414,8 +3418,8 @@ function BillSubmissionForm({
3414
3418
  bill.serviceLines.map((line, index) => /* @__PURE__ */ jsxs6("div", { className: "mbsf-line", children: [
3415
3419
  /* @__PURE__ */ jsxs6("div", { "data-label": "Procedure code", "data-field-path": `serviceLines.${index}.code`, "data-invalid": Boolean(errors[`serviceLines.${index}.code`]), children: [
3416
3420
  /* @__PURE__ */ jsx6(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) => {
3417
- const auto = evaluationModifier(evaluationType, option.id);
3418
- setLine(index, { code: option.id, ...auto ? { modifiers: [auto, ...(line.modifiers ?? []).filter((item) => !["94", "95", "96"].includes(item.replace(/^-/, "")))] } : line.modifiers ? { modifiers: line.modifiers } : {} });
3421
+ const [updated] = applyBillSubmissionEvaluationModifiers([{ ...line, code: option.id }], evaluationType);
3422
+ setLine(index, updated);
3419
3423
  } }),
3420
3424
  line.code ? /* @__PURE__ */ jsx6("small", { className: "mbsf-help", children: procedures.find((item) => item.code === line.code)?.description ?? "Custom CPT, HCPCS, or medical-legal code" }) : null,
3421
3425
  errors[`serviceLines.${index}.code`] ? /* @__PURE__ */ jsx6("small", { className: "mbsf-error", role: "alert", children: errors[`serviceLines.${index}.code`] }) : null
@@ -8397,9 +8401,408 @@ function ConnectedBillingWorkspace({ appearance, className, style, initialView =
8397
8401
  ] });
8398
8402
  }
8399
8403
 
8404
+ // src/dental-draft-editor.tsx
8405
+ import { useState as useState20 } from "react";
8406
+
8407
+ // src/treatment-draft-shared.tsx
8408
+ import { useRef as useRef11, useState as useState19 } from "react";
8409
+ import { jsx as jsx23, jsxs as jsxs23 } from "react/jsx-runtime";
8410
+ function useDraftSave(onSave) {
8411
+ const pending = useRef11(false);
8412
+ const [busy, setBusy] = useState19(false);
8413
+ const [message, setMessage] = useState19("");
8414
+ const [error, setError] = useState19(null);
8415
+ const clear = () => {
8416
+ setMessage("");
8417
+ setError(null);
8418
+ };
8419
+ const save = async (value, failure) => {
8420
+ if (pending.current) return;
8421
+ clear();
8422
+ if (failure) {
8423
+ setError(failure);
8424
+ return;
8425
+ }
8426
+ pending.current = true;
8427
+ setBusy(true);
8428
+ try {
8429
+ await onSave(value);
8430
+ setMessage("Draft saved.");
8431
+ } catch {
8432
+ setError("The draft could not be saved. Review its status before retrying.");
8433
+ } finally {
8434
+ pending.current = false;
8435
+ setBusy(false);
8436
+ }
8437
+ };
8438
+ return { busy, message, error, clear, save };
8439
+ }
8440
+ function TreatmentDraftShell({ title, description, children, appearance, className = "", style }) {
8441
+ return /* @__PURE__ */ jsxs23("section", { className: `mbtd ${className}`.trim(), "aria-label": title, style: { ...mindBillAppearanceStyle(appearance), ...style }, children: [
8442
+ /* @__PURE__ */ jsx23("style", { children: `.mbtd{font:14px/1.5 ui-sans-serif,system-ui,sans-serif;color:var(--mb-text);min-width:0}.mbtd *{box-sizing:border-box}.mbtd h2{font-size:22px;margin:0 0 6px}.mbtd p{margin:6px 0 16px;color:var(--mb-muted)}.mbtd form{display:grid;gap:16px}.mbtd fieldset{margin:0;padding:16px;border:1px solid var(--mb-border);border-radius:var(--mb-radius);background:var(--mb-surface);min-width:0}.mbtd legend{font-weight:600;padding:0 6px}.mbtd .mbtd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.mbtd label{display:grid;gap:5px;font-size:13px;min-width:0}.mbtd input,.mbtd select,.mbtd textarea,.mbtd button{font:inherit;max-width:100%;min-width:0;border:1px solid var(--mb-border);border-radius:var(--mb-control-radius);padding:9px 11px;color:var(--mb-text);background:var(--mb-input);min-height:40px}.mbtd textarea{resize:vertical;min-height:76px}.mbtd button{cursor:pointer;background:var(--mb-surface)}.mbtd button:disabled{opacity:.55;cursor:default}.mbtd .mbtd-primary{background:var(--mb-accent);color:var(--mb-accent-contrast);border-color:var(--mb-accent)}.mbtd .mbtd-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px}.mbtd .mbtd-wide{grid-column:1/-1}.mbtd [role=alert]{color:var(--mb-danger)}.mbtd [role=status]{margin:0}.mbtd :is(input,select,textarea,button):focus-visible{outline:2px solid var(--mb-accent);outline-offset:2px}.mbtd .mbtd-note{padding:12px;background:var(--mb-soft);border-radius:var(--mb-control-radius)}.mbtd .mbtd-attribution{font-size:12px;text-align:right}.mbtd a{color:var(--mb-accent)}@media(max-width:520px){.mbtd .mbtd-grid{grid-template-columns:minmax(0,1fr)}.mbtd fieldset{padding:12px}}` }),
8443
+ /* @__PURE__ */ jsx23("h2", { children: title }),
8444
+ /* @__PURE__ */ jsx23("p", { children: description }),
8445
+ children,
8446
+ /* @__PURE__ */ jsx23("p", { className: "mbtd-attribution", children: /* @__PURE__ */ jsx23("a", { href: "https://mindbill.org", target: "_blank", rel: "noopener noreferrer", children: "Powered by MindBill" }) })
8447
+ ] });
8448
+ }
8449
+ function isDraftDate(value) {
8450
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
8451
+ }
8452
+
8453
+ // src/dental-draft-editor.tsx
8454
+ import { jsx as jsx24, jsxs as jsxs24 } from "react/jsx-runtime";
8455
+ function dentalDraftChargeSummary(lines) {
8456
+ const knownChargeCents = lines.reduce((sum, line) => sum + (line.chargeCents ?? 0), 0);
8457
+ return { knownChargeCents, totalChargeCents: lines.some((line) => line.chargeCents === null) ? null : knownChargeCents };
8458
+ }
8459
+ function parseDentalCharge(value) {
8460
+ if (!value.trim()) return null;
8461
+ if (!/^\d+(?:\.\d{1,2})?$/.test(value)) throw new Error("Enter a positive charge with at most two decimal places, or leave it blank.");
8462
+ const [whole = "0", fraction = ""] = value.split(".");
8463
+ const cents = Number(whole) * 100 + Number(fraction.padEnd(2, "0"));
8464
+ if (!Number.isSafeInteger(cents) || cents < 1 || cents > 1e9) throw new Error("Enter a charge between $0.01 and $10,000,000, or leave it blank.");
8465
+ return cents;
8466
+ }
8467
+ function validateDentalDraftContent(content) {
8468
+ if (!content.renderingProviderId.trim() || content.renderingProviderId.length > 255) return "Choose a rendering provider in the host application.";
8469
+ if (content.billingProviderId != null && (!content.billingProviderId.trim() || content.billingProviderId.length > 255)) return "Choose a valid billing provider in the host application.";
8470
+ if (content.lines.length > 100) return "A draft can contain up to 100 service lines.";
8471
+ if (content.diagnosisCodes.length > 12 || content.diagnosisCodes.some((code) => !code.trim() || code.length > 20)) return "Enter up to 12 diagnosis codes of at most 20 characters each.";
8472
+ if (content.notes !== null && (!content.notes.trim() || content.notes.length > 5e3)) return "Notes must contain between 1 and 5,000 characters.";
8473
+ for (const [index, line] of content.lines.entries()) {
8474
+ const prefix = `Line ${index + 1}: `;
8475
+ if (line.code !== null && !/^D[0-9]{4}$/.test(line.code)) return prefix + "enter D followed by four digits, or leave the code blank.";
8476
+ if (!Number.isInteger(line.quantity) || line.quantity < 1 || line.quantity > 1e5) return prefix + "enter a whole quantity between 1 and 100,000.";
8477
+ if (line.chargeCents !== null && (!Number.isSafeInteger(line.chargeCents) || line.chargeCents < 1 || line.chargeCents > 1e9)) return prefix + "enter a positive extended charge, or leave it unknown.";
8478
+ if (line.serviceDate !== null && !isDraftDate(line.serviceDate)) return prefix + "enter a valid service date.";
8479
+ if (line.editionYear !== null && (!Number.isInteger(line.editionYear) || line.editionYear < 1900 || line.editionYear > 2200)) return prefix + "enter an edition year between 1900 and 2200.";
8480
+ for (const [value, limit] of [[line.description, 1e3], [line.chargeReference, 1e3], [line.oralCavity, 100], [line.prosthesisNotes, 2e3]]) {
8481
+ if (value !== null && (!value.trim() || value.length > limit)) return prefix + `text must contain between 1 and ${limit} characters.`;
8482
+ }
8483
+ if (line.teeth.length > 32 || line.surfaces.length > 10 || [...line.teeth, ...line.surfaces].some((value) => !value.trim() || value.length > 20)) return prefix + "check tooth and surface entries (up to 32 teeth and 10 surfaces, 20 characters each).";
8484
+ }
8485
+ return null;
8486
+ }
8487
+ var blankLine2 = () => ({ code: null, description: null, editionYear: null, serviceDate: null, quantity: 1, chargeCents: null, chargeReference: null, teeth: [], surfaces: [], oralCavity: null, prosthesisNotes: null });
8488
+ var nullable = (value) => value === "" ? null : value;
8489
+ var split = (value) => value.split(",").map((part) => part.trim()).filter(Boolean);
8490
+ var currency2 = (cents) => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(cents / 100);
8491
+ function DentalDraftEditor({ initialContent, onSave, disabled = false, ...appearance }) {
8492
+ const [content, setContent] = useState20(() => structuredClone(initialContent));
8493
+ const [charges, setCharges] = useState20(() => initialContent.lines.map((line) => line.chargeCents === null ? "" : (line.chargeCents / 100).toFixed(2)));
8494
+ const [diagnoses, setDiagnoses] = useState20(() => initialContent.diagnosisCodes.join(", "));
8495
+ const [teeth, setTeeth] = useState20(() => initialContent.lines.map((line) => line.teeth.join(", ")));
8496
+ const [surfaces, setSurfaces] = useState20(() => initialContent.lines.map((line) => line.surfaces.join(", ")));
8497
+ const action = useDraftSave(onSave);
8498
+ const locked = disabled || action.busy;
8499
+ const updateLine = (index, patch) => {
8500
+ action.clear();
8501
+ setContent((current) => ({ ...current, lines: current.lines.map((line, position) => position === index ? { ...line, ...patch } : line) }));
8502
+ };
8503
+ const parsed = () => ({ ...content, diagnosisCodes: split(diagnoses), lines: content.lines.map((line, index) => ({ ...line, teeth: split(teeth[index] ?? ""), surfaces: split(surfaces[index] ?? ""), chargeCents: parseDentalCharge(charges[index] ?? "") })) });
8504
+ let summary = null;
8505
+ try {
8506
+ summary = dentalDraftChargeSummary(parsed().lines);
8507
+ } catch {
8508
+ }
8509
+ return /* @__PURE__ */ jsxs24(TreatmentDraftShell, { ...appearance, title: "Dental draft", description: "Record dental services, tooth details, and practice charges for review.", children: [
8510
+ /* @__PURE__ */ jsx24("p", { className: "mbtd-note", children: "Codes and prices need review. Saving does not send a dental claim. A blank charge remains unknown." }),
8511
+ /* @__PURE__ */ jsxs24("form", { onSubmit: (event) => {
8512
+ event.preventDefault();
8513
+ if (locked) return;
8514
+ let next;
8515
+ try {
8516
+ next = parsed();
8517
+ } catch {
8518
+ void action.save(content, "Enter positive charges with at most two decimal places, or leave them blank.");
8519
+ return;
8520
+ }
8521
+ void action.save(next, validateDentalDraftContent(next));
8522
+ }, children: [
8523
+ /* @__PURE__ */ jsxs24("fieldset", { disabled: locked, children: [
8524
+ /* @__PURE__ */ jsx24("legend", { children: "Claim details" }),
8525
+ /* @__PURE__ */ jsxs24("div", { className: "mbtd-grid", children: [
8526
+ /* @__PURE__ */ jsxs24("label", { children: [
8527
+ "Diagnosis codes, comma separated",
8528
+ /* @__PURE__ */ jsx24("input", { value: diagnoses, onChange: (event) => {
8529
+ action.clear();
8530
+ setDiagnoses(event.target.value);
8531
+ } })
8532
+ ] }),
8533
+ /* @__PURE__ */ jsxs24("label", { children: [
8534
+ "Notes",
8535
+ /* @__PURE__ */ jsx24("textarea", { maxLength: 5e3, value: content.notes ?? "", onChange: (event) => {
8536
+ action.clear();
8537
+ setContent({ ...content, notes: nullable(event.target.value) });
8538
+ } })
8539
+ ] })
8540
+ ] })
8541
+ ] }),
8542
+ content.lines.map((line, index) => /* @__PURE__ */ jsxs24("fieldset", { disabled: locked, children: [
8543
+ /* @__PURE__ */ jsxs24("legend", { children: [
8544
+ "Service ",
8545
+ index + 1
8546
+ ] }),
8547
+ /* @__PURE__ */ jsxs24("div", { className: "mbtd-grid", children: [
8548
+ /* @__PURE__ */ jsxs24("label", { children: [
8549
+ "Dental code",
8550
+ /* @__PURE__ */ jsx24("input", { placeholder: "D0120", maxLength: 5, value: line.code ?? "", onChange: (event) => updateLine(index, { code: nullable(event.target.value.toUpperCase()) }) })
8551
+ ] }),
8552
+ /* @__PURE__ */ jsxs24("label", { children: [
8553
+ "Code edition year",
8554
+ /* @__PURE__ */ jsx24("input", { type: "number", min: 1900, max: 2200, value: line.editionYear ?? "", onChange: (event) => updateLine(index, { editionYear: event.target.value === "" ? null : Number(event.target.value) }) })
8555
+ ] }),
8556
+ /* @__PURE__ */ jsxs24("label", { children: [
8557
+ "Date of service",
8558
+ /* @__PURE__ */ jsx24("input", { type: "date", value: line.serviceDate ?? "", onChange: (event) => updateLine(index, { serviceDate: nullable(event.target.value) }) })
8559
+ ] }),
8560
+ /* @__PURE__ */ jsxs24("label", { children: [
8561
+ "Quantity",
8562
+ /* @__PURE__ */ jsx24("input", { type: "number", min: 1, max: 1e5, step: 1, value: line.quantity, onChange: (event) => updateLine(index, { quantity: Number(event.target.value) }) })
8563
+ ] }),
8564
+ /* @__PURE__ */ jsxs24("label", { children: [
8565
+ "Extended line charge ($)",
8566
+ /* @__PURE__ */ jsx24("input", { inputMode: "decimal", placeholder: "Unknown", value: charges[index] ?? "", onChange: (event) => {
8567
+ action.clear();
8568
+ setCharges(charges.map((value, position) => position === index ? event.target.value : value));
8569
+ } }),
8570
+ /* @__PURE__ */ jsx24("small", { children: "Includes all units. This is the practice charge, not a fee allowance." })
8571
+ ] }),
8572
+ /* @__PURE__ */ jsxs24("label", { children: [
8573
+ "Charge source / reference",
8574
+ /* @__PURE__ */ jsx24("input", { maxLength: 1e3, value: line.chargeReference ?? "", onChange: (event) => updateLine(index, { chargeReference: nullable(event.target.value) }) })
8575
+ ] }),
8576
+ /* @__PURE__ */ jsxs24("label", { children: [
8577
+ "Teeth, comma separated",
8578
+ /* @__PURE__ */ jsx24("input", { value: teeth[index] ?? "", onChange: (event) => {
8579
+ action.clear();
8580
+ setTeeth(teeth.map((value, position) => position === index ? event.target.value : value));
8581
+ } })
8582
+ ] }),
8583
+ /* @__PURE__ */ jsxs24("label", { children: [
8584
+ "Surfaces, comma separated",
8585
+ /* @__PURE__ */ jsx24("input", { value: surfaces[index] ?? "", onChange: (event) => {
8586
+ action.clear();
8587
+ setSurfaces(surfaces.map((value, position) => position === index ? event.target.value.toUpperCase() : value));
8588
+ } })
8589
+ ] }),
8590
+ /* @__PURE__ */ jsxs24("label", { children: [
8591
+ "Oral cavity",
8592
+ /* @__PURE__ */ jsx24("input", { maxLength: 100, value: line.oralCavity ?? "", onChange: (event) => updateLine(index, { oralCavity: nullable(event.target.value) }) })
8593
+ ] }),
8594
+ /* @__PURE__ */ jsxs24("label", { children: [
8595
+ "Service description",
8596
+ /* @__PURE__ */ jsx24("input", { maxLength: 1e3, value: line.description ?? "", onChange: (event) => updateLine(index, { description: nullable(event.target.value) }) })
8597
+ ] }),
8598
+ /* @__PURE__ */ jsxs24("label", { className: "mbtd-wide", children: [
8599
+ "Prosthesis notes",
8600
+ /* @__PURE__ */ jsx24("textarea", { maxLength: 2e3, value: line.prosthesisNotes ?? "", onChange: (event) => updateLine(index, { prosthesisNotes: nullable(event.target.value) }) })
8601
+ ] })
8602
+ ] }),
8603
+ /* @__PURE__ */ jsx24("div", { className: "mbtd-actions", children: /* @__PURE__ */ jsxs24("button", { type: "button", onClick: () => {
8604
+ action.clear();
8605
+ setContent({ ...content, lines: content.lines.filter((_, position) => position !== index) });
8606
+ setCharges(charges.filter((_, position) => position !== index));
8607
+ setTeeth(teeth.filter((_, position) => position !== index));
8608
+ setSurfaces(surfaces.filter((_, position) => position !== index));
8609
+ }, children: [
8610
+ "Remove service ",
8611
+ index + 1
8612
+ ] }) })
8613
+ ] }, index)),
8614
+ /* @__PURE__ */ jsx24("div", { children: /* @__PURE__ */ jsx24("button", { type: "button", disabled: locked || content.lines.length >= 100, onClick: () => {
8615
+ action.clear();
8616
+ setContent({ ...content, lines: [...content.lines, blankLine2()] });
8617
+ setCharges([...charges, ""]);
8618
+ setTeeth([...teeth, ""]);
8619
+ setSurfaces([...surfaces, ""]);
8620
+ }, children: "Add dental service" }) }),
8621
+ /* @__PURE__ */ jsx24("p", { role: "status", children: summary ? summary.totalChargeCents === null ? `Known charges: ${currency2(summary.knownChargeCents)} \xB7 Total incomplete` : `Total practice charge: ${currency2(summary.totalChargeCents)}` : "Review charge amounts before saving." }),
8622
+ action.error ? /* @__PURE__ */ jsx24("p", { role: "alert", children: action.error }) : null,
8623
+ /* @__PURE__ */ jsxs24("div", { className: "mbtd-actions", children: [
8624
+ /* @__PURE__ */ jsx24("button", { type: "submit", className: "mbtd-primary", disabled: locked, children: action.busy ? "Saving\u2026" : "Save dental draft" }),
8625
+ /* @__PURE__ */ jsx24("p", { role: "status", children: action.message })
8626
+ ] })
8627
+ ] })
8628
+ ] });
8629
+ }
8630
+
8631
+ // src/rfa-draft-form.tsx
8632
+ import { useState as useState21 } from "react";
8633
+ import { jsx as jsx25, jsxs as jsxs25 } from "react/jsx-runtime";
8634
+ function normalizeRfaDraft(draft) {
8635
+ const copy = structuredClone(draft);
8636
+ for (const item of copy.items) {
8637
+ for (const key of ["externalId", "procedureCode", "frequency", "duration", "requestedFrom", "requestedTo"]) {
8638
+ if (item[key] !== void 0 && !item[key]?.trim()) delete item[key];
8639
+ }
8640
+ }
8641
+ delete copy.signedAt;
8642
+ return copy;
8643
+ }
8644
+ function validateRfaDraft(draft) {
8645
+ for (const value of [draft.claimId, draft.patientId, draft.renderingProviderId]) if (!value.trim() || value.length > 255) return "Select a claim, patient, and rendering provider in the host application.";
8646
+ for (const value of [draft.externalId, draft.claimsAdminId]) if (value !== void 0 && (!value.trim() || value.length > 255)) return "Check the request and claims administrator identifiers.";
8647
+ for (const value of [draft.employeeName, draft.providerName]) if (!value.trim() || value.length > 200) return "Employee and provider names are required (up to 200 characters).";
8648
+ if (draft.requestType !== void 0 && !["new", "resubmission_material_change", "oral_authorization_confirmation"].includes(draft.requestType)) return "Choose a valid request type.";
8649
+ if (draft.reviewType !== void 0 && !["prospective", "concurrent", "retrospective"].includes(draft.reviewType)) return "Choose a valid review type.";
8650
+ if (draft.requestType === "resubmission_material_change" && !draft.materialChange?.trim()) return "Describe the material change for this resubmission.";
8651
+ if ((draft.rationale?.length ?? 0) > 2e4 || (draft.materialChange?.length ?? 0) > 2e4) return "Rationale and material change must each be at most 20,000 characters.";
8652
+ if (draft.placeOfServiceCode !== void 0 && !/^\d{2}$/.test(draft.placeOfServiceCode)) return "Place of service must contain two digits.";
8653
+ if ((draft.providerNpi?.length ?? 0) > 20 || (draft.providerPhone?.length ?? 0) > 30 || (draft.claimNumber?.length ?? 0) > 100) return "Check provider contact and claim number lengths.";
8654
+ if (draft.providerFax && (draft.providerFax.length > 30 || !/^[+\d().\s-]+$/.test(draft.providerFax) || !/^\d{10,15}$/.test(draft.providerFax.replace(/\D/g, "")))) return "Enter a return fax with 10 to 15 digits, or leave it blank.";
8655
+ if (draft.dateOfInjury !== void 0 && !isDraftDate(draft.dateOfInjury)) return "Check the injury date in the host application.";
8656
+ if (draft.items.length < 1 || draft.items.length > 100) return "Include between 1 and 100 requested services.";
8657
+ for (const [index, item] of draft.items.entries()) {
8658
+ const prefix = `Service ${index + 1}: `;
8659
+ if (!item.diagnosisCode.trim() || item.diagnosisCode.length > 16 || !item.serviceDescription.trim() || item.serviceDescription.length > 1e3) return prefix + "a diagnosis code and service description are required.";
8660
+ for (const [value, max] of [[item.externalId, 255], [item.procedureCode, 16], [item.frequency, 200], [item.duration, 200]]) if (value !== void 0 && (!value.trim() || value.length > max)) return prefix + `optional text must contain 1 to ${max} characters.`;
8661
+ if (item.units !== void 0 && (!Number.isInteger(item.units) || item.units < 1 || item.units > 1e5)) return prefix + "units must be a whole number between 1 and 100,000.";
8662
+ if (item.quantity !== void 0 && (!Number.isFinite(item.quantity) || item.quantity <= 0 || item.quantity > 1e5)) return prefix + "quantity must be positive and at most 100,000.";
8663
+ if (item.requestedFrom !== void 0 && !isDraftDate(item.requestedFrom) || item.requestedTo !== void 0 && (!isDraftDate(item.requestedTo) || !item.requestedFrom || item.requestedTo < item.requestedFrom)) return prefix + "enter valid dates with the end on or after the start.";
8664
+ }
8665
+ return null;
8666
+ }
8667
+ function RfaDraftForm({ initialDraft, onSave, disabled = false, ...appearance }) {
8668
+ const [draft, setDraft] = useState21(() => normalizeRfaDraft(initialDraft));
8669
+ const action = useDraftSave(onSave), locked = disabled || action.busy;
8670
+ const update = (patch) => {
8671
+ action.clear();
8672
+ setDraft((current) => ({ ...current, ...patch }));
8673
+ };
8674
+ const updateItem = (index, patch) => {
8675
+ action.clear();
8676
+ setDraft((current) => ({ ...current, items: current.items.map((item, position) => position === index ? { ...item, ...patch } : item) }));
8677
+ };
8678
+ const updateNumber = (index, key, value) => {
8679
+ action.clear();
8680
+ setDraft((current) => ({ ...current, items: current.items.map((item, position) => {
8681
+ if (position !== index) return item;
8682
+ const next = { ...item };
8683
+ if (value === "") delete next[key];
8684
+ else next[key] = Number(value);
8685
+ return next;
8686
+ }) }));
8687
+ };
8688
+ return /* @__PURE__ */ jsxs25(TreatmentDraftShell, { ...appearance, title: "Request for authorization", description: "Prepare services and supporting rationale for utilization review.", children: [
8689
+ /* @__PURE__ */ jsx25("p", { className: "mbtd-note", children: "Saving creates an unsigned draft. It does not send the request or authorize treatment." }),
8690
+ /* @__PURE__ */ jsxs25("form", { onSubmit: (event) => {
8691
+ event.preventDefault();
8692
+ if (locked) return;
8693
+ const next = normalizeRfaDraft(draft);
8694
+ void action.save(next, validateRfaDraft(next));
8695
+ }, children: [
8696
+ /* @__PURE__ */ jsxs25("fieldset", { disabled: locked, children: [
8697
+ /* @__PURE__ */ jsx25("legend", { children: "Request details" }),
8698
+ /* @__PURE__ */ jsxs25("div", { className: "mbtd-grid", children: [
8699
+ /* @__PURE__ */ jsxs25("label", { children: [
8700
+ "Employee",
8701
+ /* @__PURE__ */ jsx25("input", { readOnly: true, value: draft.employeeName })
8702
+ ] }),
8703
+ /* @__PURE__ */ jsxs25("label", { children: [
8704
+ "Requesting provider",
8705
+ /* @__PURE__ */ jsx25("input", { readOnly: true, value: draft.providerName })
8706
+ ] }),
8707
+ /* @__PURE__ */ jsxs25("label", { children: [
8708
+ "Request type",
8709
+ /* @__PURE__ */ jsxs25("select", { value: draft.requestType ?? "new", onChange: (event) => update({ requestType: event.target.value }), children: [
8710
+ /* @__PURE__ */ jsx25("option", { value: "new", children: "New request" }),
8711
+ /* @__PURE__ */ jsx25("option", { value: "resubmission_material_change", children: "Resubmission with material change" }),
8712
+ /* @__PURE__ */ jsx25("option", { value: "oral_authorization_confirmation", children: "Confirm oral authorization" })
8713
+ ] })
8714
+ ] }),
8715
+ /* @__PURE__ */ jsxs25("label", { children: [
8716
+ "Review type",
8717
+ /* @__PURE__ */ jsxs25("select", { value: draft.reviewType ?? "prospective", onChange: (event) => update({ reviewType: event.target.value }), children: [
8718
+ /* @__PURE__ */ jsx25("option", { value: "prospective", children: "Prospective \u2014 before treatment" }),
8719
+ /* @__PURE__ */ jsx25("option", { value: "concurrent", children: "Concurrent \u2014 during treatment" }),
8720
+ /* @__PURE__ */ jsx25("option", { value: "retrospective", children: "Retrospective \u2014 after treatment" })
8721
+ ] })
8722
+ ] }),
8723
+ /* @__PURE__ */ jsxs25("label", { children: [
8724
+ "Review priority",
8725
+ /* @__PURE__ */ jsxs25("select", { value: draft.expedited ? "expedited" : "standard", onChange: (event) => update({ expedited: event.target.value === "expedited" }), children: [
8726
+ /* @__PURE__ */ jsx25("option", { value: "standard", children: "Standard" }),
8727
+ /* @__PURE__ */ jsx25("option", { value: "expedited", children: "Expedited review requested" })
8728
+ ] })
8729
+ ] }),
8730
+ /* @__PURE__ */ jsxs25("label", { children: [
8731
+ "Return fax",
8732
+ /* @__PURE__ */ jsx25("input", { maxLength: 30, value: draft.providerFax ?? "", onChange: (event) => update({ providerFax: event.target.value }) }),
8733
+ /* @__PURE__ */ jsx25("small", { children: "Review the return contact. Organization defaults are not applied automatically." })
8734
+ ] }),
8735
+ /* @__PURE__ */ jsxs25("label", { className: "mbtd-wide", children: [
8736
+ "Clinical rationale",
8737
+ /* @__PURE__ */ jsx25("textarea", { maxLength: 2e4, value: draft.rationale ?? "", onChange: (event) => update({ rationale: event.target.value }) })
8738
+ ] }),
8739
+ draft.requestType === "resubmission_material_change" ? /* @__PURE__ */ jsxs25("label", { className: "mbtd-wide", children: [
8740
+ "Material change (required)",
8741
+ /* @__PURE__ */ jsx25("textarea", { required: true, maxLength: 2e4, value: draft.materialChange ?? "", onChange: (event) => update({ materialChange: event.target.value }) })
8742
+ ] }) : null
8743
+ ] })
8744
+ ] }),
8745
+ draft.items.map((item, index) => /* @__PURE__ */ jsxs25("fieldset", { disabled: locked, children: [
8746
+ /* @__PURE__ */ jsxs25("legend", { children: [
8747
+ "Requested service ",
8748
+ index + 1
8749
+ ] }),
8750
+ /* @__PURE__ */ jsxs25("div", { className: "mbtd-grid", children: [
8751
+ /* @__PURE__ */ jsxs25("label", { children: [
8752
+ "Diagnosis code",
8753
+ /* @__PURE__ */ jsx25("input", { required: true, maxLength: 16, value: item.diagnosisCode, onChange: (event) => updateItem(index, { diagnosisCode: event.target.value }) })
8754
+ ] }),
8755
+ /* @__PURE__ */ jsxs25("label", { children: [
8756
+ "Procedure code",
8757
+ /* @__PURE__ */ jsx25("input", { maxLength: 16, value: item.procedureCode ?? "", onChange: (event) => updateItem(index, { procedureCode: event.target.value }) })
8758
+ ] }),
8759
+ /* @__PURE__ */ jsxs25("label", { className: "mbtd-wide", children: [
8760
+ "Service description",
8761
+ /* @__PURE__ */ jsx25("textarea", { required: true, maxLength: 1e3, value: item.serviceDescription, onChange: (event) => updateItem(index, { serviceDescription: event.target.value }) })
8762
+ ] }),
8763
+ /* @__PURE__ */ jsxs25("label", { children: [
8764
+ "Quantity",
8765
+ /* @__PURE__ */ jsx25("input", { type: "number", min: 1e-3, max: 1e5, step: "any", value: item.quantity ?? "", onChange: (event) => updateNumber(index, "quantity", event.target.value) })
8766
+ ] }),
8767
+ /* @__PURE__ */ jsxs25("label", { children: [
8768
+ "Units",
8769
+ /* @__PURE__ */ jsx25("input", { type: "number", min: 1, max: 1e5, step: 1, value: item.units ?? "", onChange: (event) => updateNumber(index, "units", event.target.value) })
8770
+ ] }),
8771
+ /* @__PURE__ */ jsxs25("label", { children: [
8772
+ "Frequency",
8773
+ /* @__PURE__ */ jsx25("input", { maxLength: 200, value: item.frequency ?? "", onChange: (event) => updateItem(index, { frequency: event.target.value }) })
8774
+ ] }),
8775
+ /* @__PURE__ */ jsxs25("label", { children: [
8776
+ "Duration",
8777
+ /* @__PURE__ */ jsx25("input", { maxLength: 200, value: item.duration ?? "", onChange: (event) => updateItem(index, { duration: event.target.value }) })
8778
+ ] }),
8779
+ /* @__PURE__ */ jsxs25("label", { children: [
8780
+ "Requested from",
8781
+ /* @__PURE__ */ jsx25("input", { type: "date", value: item.requestedFrom ?? "", onChange: (event) => updateItem(index, { requestedFrom: event.target.value }) })
8782
+ ] }),
8783
+ /* @__PURE__ */ jsxs25("label", { children: [
8784
+ "Requested through",
8785
+ /* @__PURE__ */ jsx25("input", { type: "date", value: item.requestedTo ?? "", onChange: (event) => updateItem(index, { requestedTo: event.target.value }) })
8786
+ ] })
8787
+ ] }),
8788
+ /* @__PURE__ */ jsxs25("button", { type: "button", disabled: draft.items.length === 1, onClick: () => update({ items: draft.items.filter((_, position) => position !== index) }), children: [
8789
+ "Remove requested service ",
8790
+ index + 1
8791
+ ] })
8792
+ ] }, index)),
8793
+ /* @__PURE__ */ jsx25("div", { children: /* @__PURE__ */ jsx25("button", { type: "button", disabled: locked || draft.items.length >= 100, onClick: () => update({ items: [...draft.items, { diagnosisCode: "", serviceDescription: "" }] }), children: "Add requested service" }) }),
8794
+ action.error ? /* @__PURE__ */ jsx25("p", { role: "alert", children: action.error }) : null,
8795
+ /* @__PURE__ */ jsxs25("div", { className: "mbtd-actions", children: [
8796
+ /* @__PURE__ */ jsx25("button", { type: "submit", className: "mbtd-primary", disabled: locked, children: action.busy ? "Saving\u2026" : "Save RFA draft" }),
8797
+ /* @__PURE__ */ jsx25("p", { role: "status", children: action.message })
8798
+ ] })
8799
+ ] })
8800
+ ] });
8801
+ }
8802
+
8400
8803
  // src/index.tsx
8401
8804
  function widget(tagName, props) {
8402
- const ref = useRef11(null);
8805
+ const ref = useRef12(null);
8403
8806
  const { onMindBill, onMindBillError } = props;
8404
8807
  useEffect16(() => {
8405
8808
  const element = ref.current;
@@ -8488,6 +8891,7 @@ export {
8488
8891
  DEFAULT_BILL_SUBMISSION_MODIFIERS,
8489
8892
  DEFAULT_BILL_SUBMISSION_PROCEDURES,
8490
8893
  DEFAULT_BILL_SUBMISSION_TAXONOMIES,
8894
+ DentalDraftEditor,
8491
8895
  MED_LEGAL_REPORT_TYPE_CODE,
8492
8896
  MindBillBillReview,
8493
8897
  MindBillBillTimeline,
@@ -8497,6 +8901,7 @@ export {
8497
8901
  PSYCH_QME_DEFAULT_DIAGNOSIS,
8498
8902
  REPORT_BILL_STATUS_OPTIONS2 as REPORT_BILL_STATUS_OPTIONS,
8499
8903
  ReportBillStatusDialog,
8904
+ RfaDraftForm,
8500
8905
  SECOND_REVIEW_REASON_TEMPLATE,
8501
8906
  SECOND_REVIEW_REASON_TEMPLATES,
8502
8907
  SECOND_REVIEW_WINDOW_DAYS,
@@ -8525,6 +8930,7 @@ export {
8525
8930
  createBillLifecycleClient2 as createBillLifecycleClient,
8526
8931
  createBillStatusClient,
8527
8932
  createBillingOperationsClient,
8933
+ dentalDraftChargeSummary,
8528
8934
  ensureTrailingBillSubmissionLine,
8529
8935
  ensureTrailingProcedureLine,
8530
8936
  exactClaimsAdministratorMatch,
@@ -8535,8 +8941,10 @@ export {
8535
8941
  mindBillAppearanceStyle,
8536
8942
  mindBillThemePresets,
8537
8943
  normalizeClaimsAdministratorName,
8944
+ normalizeRfaDraft,
8538
8945
  organizationProfileOptions,
8539
8946
  parseBillSubmissionDate,
8947
+ parseDentalCharge,
8540
8948
  prepareBillSubmissionDocuments,
8541
8949
  reportBillStatusContacts2 as reportBillStatusContacts,
8542
8950
  resolveMindBillAppearance,
@@ -8546,6 +8954,8 @@ export {
8546
8954
  useBillLifecycle,
8547
8955
  useBillStatus,
8548
8956
  validateBillSubmission,
8957
+ validateDentalDraftContent,
8958
+ validateRfaDraft,
8549
8959
  visibleBillLifecycleActions
8550
8960
  };
8551
8961
  //# sourceMappingURL=index.js.map