@mindbill/angular 0.6.0 → 0.8.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, EventEmitter, effect, Output, Input, Component, Directive } from '@angular/core';
2
+ import { signal, computed, EventEmitter, effect, Output, Input, Component, Directive, inject, ChangeDetectorRef, HostListener } from '@angular/core';
3
3
  import { createBillLifecycleClient, createBillReferenceClient, createBillSubmissionClient } from '@mindbill/browser';
4
4
  import * as i1 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
@@ -275,7 +275,7 @@ function ensureTrailingProcedureLine(lines) {
275
275
  : entered;
276
276
  }
277
277
 
278
- const THEME$2 = {
278
+ const THEME$3 = {
279
279
  mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
280
280
  "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
281
281
  "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
@@ -349,7 +349,7 @@ function buildMindBillReportCsv(rows) {
349
349
  class DashboardBase {
350
350
  appearance = { preset: "mindbill" };
351
351
  get themeStyle() {
352
- const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
352
+ const base = THEME$3[this.appearance.preset ?? "mindbill"] ?? THEME$3["mindbill"];
353
353
  return {
354
354
  "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
355
355
  "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
@@ -454,6 +454,444 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
454
454
  type: Input
455
455
  }] } });
456
456
 
457
+ const MINDBILL_STATUS_AGING_BUCKETS = [
458
+ { id: "current", label: "0–30 days" },
459
+ { id: "31-60", label: "31–60 days" },
460
+ { id: "61-90", label: "61–90 days" },
461
+ { id: "91+", label: "91+ days" },
462
+ ];
463
+ const DEFAULT_STATE_ORDER = [
464
+ "draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
465
+ "paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
466
+ ];
467
+ const agingDays = (bill, now) => {
468
+ if (bill.agingDays != null)
469
+ return Math.max(0, Math.floor(bill.agingDays));
470
+ const source = bill.submittedAt ?? bill.updatedAt;
471
+ if (!source)
472
+ return 0;
473
+ const time = new Date(source).getTime();
474
+ return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
475
+ };
476
+ const agingBucketIndex = (days) => (days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3);
477
+ const stateLabel = (state) => state.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
478
+ const emptyCell = (state, bucket) => ({ state, bucket, count: 0, balance: 0, bills: [] });
479
+ function buildMindBillStatusAgingMatrix(bills, stateOrder = DEFAULT_STATE_ORDER, now = new Date()) {
480
+ const orderIndex = new Map(stateOrder.map((state, index) => [state.toLowerCase(), index]));
481
+ const states = [...new Set(bills.map((bill) => bill.state))].sort((a, b) => {
482
+ const left = orderIndex.get(a.toLowerCase()) ?? stateOrder.length;
483
+ const right = orderIndex.get(b.toLowerCase()) ?? stateOrder.length;
484
+ return left - right || a.localeCompare(b);
485
+ });
486
+ const columnTotals = MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell("all", bucket.id));
487
+ const grandTotal = emptyCell("all", "total");
488
+ const rows = states.map((state) => ({
489
+ state,
490
+ label: stateLabel(state),
491
+ cells: MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell(state, bucket.id)),
492
+ total: emptyCell(state, "total"),
493
+ }));
494
+ const rowByState = new Map(rows.map((row) => [row.state, row]));
495
+ for (const bill of bills) {
496
+ const row = rowByState.get(bill.state);
497
+ const bucketIndex = agingBucketIndex(agingDays(bill, now));
498
+ for (const cell of [row.cells[bucketIndex], row.total, columnTotals[bucketIndex], grandTotal]) {
499
+ cell.count += 1;
500
+ cell.balance += bill.balanceDue;
501
+ cell.bills.push(bill);
502
+ }
503
+ }
504
+ return { rows, columnTotals, grandTotal };
505
+ }
506
+ function buildMindBillStatusAgingCsv(bills, stateOrder) {
507
+ const matrix = buildMindBillStatusAgingMatrix(bills, stateOrder);
508
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
509
+ const header = ["Status", ...MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => bucket.label), "Total"];
510
+ const lines = matrix.rows.map((row) => [row.label, ...row.cells.map((cell) => cell.count), row.total.count]);
511
+ lines.push(["Total", ...matrix.columnTotals.map((cell) => cell.count), matrix.grandTotal.count]);
512
+ return [header, ...lines].map((line) => line.map(quote).join(",")).join("\n");
513
+ }
514
+
515
+ const THEME$2 = {
516
+ mindbill: { a: "#238dbd", ac: "#fff", bg: "#f3f8fa", s: "#fff", t: "#203743", m: "#657982", b: "#dbe6ea", r: "14px", cr: "8px", font: "Inter,system-ui,sans-serif" },
517
+ "qme-companion": { a: "#53b5dc", ac: "#173542", bg: "#f2f8fb", s: "#fff", t: "#1d3440", m: "#617783", b: "#d7e5eb", r: "12px", cr: "8px", font: "Inter,system-ui,sans-serif" },
518
+ "orange-bright": { a: "#f4510b", ac: "#fff", bg: "#fffaf6", s: "#fffefd", t: "#090f1f", m: "#626a73", b: "#e7e1da", r: "16px", cr: "10px", font: "Inter,system-ui,sans-serif" },
519
+ "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
520
+ };
521
+ class MindBillStatusAgingMatrixComponent {
522
+ bills = [];
523
+ heading = "Bills by status and age";
524
+ description = "Every bill grouped by lifecycle status, then by days outstanding.";
525
+ stateOrder;
526
+ showBalances = true;
527
+ appearance = { preset: "mindbill" };
528
+ cellSelected = new EventEmitter();
529
+ buckets = MINDBILL_STATUS_AGING_BUCKETS;
530
+ get matrix() {
531
+ return buildMindBillStatusAgingMatrix(this.bills, this.stateOrder);
532
+ }
533
+ get themeStyle() {
534
+ const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
535
+ return {
536
+ "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
537
+ "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
538
+ "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
539
+ "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
540
+ "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
541
+ };
542
+ }
543
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
544
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillStatusAgingMatrixComponent, isStandalone: true, selector: "mindbill-status-aging-matrix", inputs: { bills: "bills", heading: "heading", description: "description", stateOrder: "stateOrder", showBalances: "showBalances", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
545
+ <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
546
+ <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
547
+ <div class="card">
548
+ <table>
549
+ <thead>
550
+ <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
551
+ </thead>
552
+ <tbody>
553
+ @for (row of matrix.rows; track row.state) {
554
+ <tr>
555
+ <td class="status"><i class="pill">{{ row.label }}</i></td>
556
+ @for (cell of row.cells; track cell.bucket) {
557
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
558
+ }
559
+ <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
560
+ </tr>
561
+ }
562
+ <tr class="totals">
563
+ <td class="status">Total</td>
564
+ @for (cell of matrix.columnTotals; track cell.bucket) {
565
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
566
+ }
567
+ <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
568
+ </tr>
569
+ </tbody>
570
+ </table>
571
+ </div>
572
+ </section>
573
+ `, isInline: true, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
574
+ }
575
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, decorators: [{
576
+ type: Component,
577
+ args: [{ selector: "mindbill-status-aging-matrix", standalone: true, imports: [CommonModule], template: `
578
+ <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
579
+ <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
580
+ <div class="card">
581
+ <table>
582
+ <thead>
583
+ <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
584
+ </thead>
585
+ <tbody>
586
+ @for (row of matrix.rows; track row.state) {
587
+ <tr>
588
+ <td class="status"><i class="pill">{{ row.label }}</i></td>
589
+ @for (cell of row.cells; track cell.bucket) {
590
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
591
+ }
592
+ <td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
593
+ </tr>
594
+ }
595
+ <tr class="totals">
596
+ <td class="status">Total</td>
597
+ @for (cell of matrix.columnTotals; track cell.bucket) {
598
+ <td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
599
+ }
600
+ <td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
601
+ </tr>
602
+ </tbody>
603
+ </table>
604
+ </div>
605
+ </section>
606
+ `, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"] }]
607
+ }], propDecorators: { bills: [{
608
+ type: Input
609
+ }], heading: [{
610
+ type: Input
611
+ }], description: [{
612
+ type: Input
613
+ }], stateOrder: [{
614
+ type: Input
615
+ }], showBalances: [{
616
+ type: Input
617
+ }], appearance: [{
618
+ type: Input
619
+ }], cellSelected: [{
620
+ type: Output
621
+ }] } });
622
+
623
+ /** Date helpers shared by the Angular submission surfaces. Mirrors the React
624
+ * package's parseBillSubmissionDate/formatBillSubmissionDate exactly so both
625
+ * frameworks accept and render the same values. */
626
+ function parseMindBillSubmissionDate(value) {
627
+ const trimmed = value.trim();
628
+ const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
629
+ const us = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/.exec(trimmed);
630
+ const digits = /^(\d{2})(\d{2})(\d{4})$/.exec(trimmed.replace(/\D/g, ""));
631
+ const match = iso ? [iso[2], iso[3], iso[1]] : us ? [us[1], us[2], us[3]] : digits ? [digits[1], digits[2], digits[3]] : null;
632
+ if (!match)
633
+ return undefined;
634
+ const month = Number(match[0]);
635
+ const day = Number(match[1]);
636
+ const year = Number(match[2]);
637
+ const date = new Date(Date.UTC(year, month - 1, day));
638
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
639
+ return undefined;
640
+ return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
641
+ }
642
+ function formatMindBillSubmissionDate(value) {
643
+ const parsed = value ? parseMindBillSubmissionDate(value) : undefined;
644
+ if (!parsed)
645
+ return "";
646
+ const [year, month, day] = parsed.split("-");
647
+ return `${month}/${day}/${year}`;
648
+ }
649
+ /** Accepts complete CPT (5 digits), HCPCS (letter + 4 digits), or California
650
+ * medical-legal codes typed directly into the procedure picker. */
651
+ function mindBillCustomProcedureOption(query) {
652
+ const code = query.trim().toUpperCase().replace(/\s+/g, "");
653
+ if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code))
654
+ return null;
655
+ return { id: code, label: code, detail: "Use this CPT, HCPCS, or medical-legal code" };
656
+ }
657
+
658
+ /** Styled searchable dropdown shared by the submission form's procedure,
659
+ * modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
660
+ * package's ComboBox: query filtering, optional custom-code creation,
661
+ * loading states, and infinite scroll via (endReached). */
662
+ class MindBillComboBoxComponent {
663
+ value = "";
664
+ placeholder = "";
665
+ options = [];
666
+ disabled = false;
667
+ loading = false;
668
+ loadingMore = false;
669
+ invalid = false;
670
+ preserveValueOnOpen = false;
671
+ ariaLabel = "";
672
+ createOption;
673
+ opened = new EventEmitter();
674
+ queryChange = new EventEmitter();
675
+ endReached = new EventEmitter();
676
+ selected = new EventEmitter();
677
+ open = false;
678
+ query = "";
679
+ closeTimer;
680
+ get visible() {
681
+ const q = this.query.trim().toLowerCase();
682
+ const matches = q
683
+ ? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
684
+ : this.options;
685
+ const custom = this.createOption?.(this.query) ?? null;
686
+ return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
687
+ ? [...matches, custom]
688
+ : matches;
689
+ }
690
+ get showMenu() {
691
+ return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
692
+ }
693
+ onFocus() {
694
+ clearTimeout(this.closeTimer);
695
+ const next = this.preserveValueOnOpen ? this.value : "";
696
+ this.open = true;
697
+ this.query = next;
698
+ this.opened.emit();
699
+ if (next)
700
+ this.queryChange.emit(next);
701
+ }
702
+ onInput(event) {
703
+ this.open = true;
704
+ this.query = event.target.value;
705
+ this.queryChange.emit(this.query);
706
+ }
707
+ onBlur() {
708
+ this.closeTimer = setTimeout(() => { this.open = false; }, 120);
709
+ }
710
+ onScroll(event) {
711
+ const menu = event.currentTarget;
712
+ if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120)
713
+ this.endReached.emit();
714
+ }
715
+ choose(option) {
716
+ this.selected.emit(option);
717
+ this.open = false;
718
+ this.query = "";
719
+ }
720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
721
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillComboBoxComponent, isStandalone: true, selector: "mindbill-combo-box", inputs: { value: "value", placeholder: "placeholder", options: "options", disabled: "disabled", loading: "loading", loadingMore: "loadingMore", invalid: "invalid", preserveValueOnOpen: "preserveValueOnOpen", ariaLabel: "ariaLabel", createOption: "createOption" }, outputs: { opened: "opened", queryChange: "queryChange", endReached: "endReached", selected: "selected" }, ngImport: i0, template: `
722
+ <div class="combo">
723
+ <input
724
+ type="text"
725
+ role="combobox"
726
+ autocomplete="off"
727
+ [attr.aria-label]="ariaLabel"
728
+ [attr.aria-invalid]="invalid || null"
729
+ [attr.aria-expanded]="showMenu"
730
+ [disabled]="disabled"
731
+ [placeholder]="placeholder"
732
+ [value]="open ? query : value"
733
+ (focus)="onFocus()"
734
+ (click)="onFocus()"
735
+ (input)="onInput($event)"
736
+ (blur)="onBlur()"
737
+ />
738
+ @if (showMenu) {
739
+ <div class="menu" role="listbox" (scroll)="onScroll($event)">
740
+ @if (loading && !visible.length) { <div class="status">Loading…</div> }
741
+ @else if (visible.length) {
742
+ @for (option of visible; track option.id) {
743
+ <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
744
+ <strong>{{ option.label }}</strong>
745
+ @if (option.detail) { <small>{{ option.detail }}</small> }
746
+ </button>
747
+ }
748
+ } @else { <div class="status">No matches</div> }
749
+ @if (loadingMore) { <div class="status">Loading more…</div> }
750
+ </div>
751
+ }
752
+ </div>
753
+ `, isInline: true, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
754
+ }
755
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, decorators: [{
756
+ type: Component,
757
+ args: [{ selector: "mindbill-combo-box", standalone: true, imports: [CommonModule], template: `
758
+ <div class="combo">
759
+ <input
760
+ type="text"
761
+ role="combobox"
762
+ autocomplete="off"
763
+ [attr.aria-label]="ariaLabel"
764
+ [attr.aria-invalid]="invalid || null"
765
+ [attr.aria-expanded]="showMenu"
766
+ [disabled]="disabled"
767
+ [placeholder]="placeholder"
768
+ [value]="open ? query : value"
769
+ (focus)="onFocus()"
770
+ (click)="onFocus()"
771
+ (input)="onInput($event)"
772
+ (blur)="onBlur()"
773
+ />
774
+ @if (showMenu) {
775
+ <div class="menu" role="listbox" (scroll)="onScroll($event)">
776
+ @if (loading && !visible.length) { <div class="status">Loading…</div> }
777
+ @else if (visible.length) {
778
+ @for (option of visible; track option.id) {
779
+ <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
780
+ <strong>{{ option.label }}</strong>
781
+ @if (option.detail) { <small>{{ option.detail }}</small> }
782
+ </button>
783
+ }
784
+ } @else { <div class="status">No matches</div> }
785
+ @if (loadingMore) { <div class="status">Loading more…</div> }
786
+ </div>
787
+ }
788
+ </div>
789
+ `, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"] }]
790
+ }], propDecorators: { value: [{
791
+ type: Input
792
+ }], placeholder: [{
793
+ type: Input
794
+ }], options: [{
795
+ type: Input
796
+ }], disabled: [{
797
+ type: Input
798
+ }], loading: [{
799
+ type: Input
800
+ }], loadingMore: [{
801
+ type: Input
802
+ }], invalid: [{
803
+ type: Input
804
+ }], preserveValueOnOpen: [{
805
+ type: Input
806
+ }], ariaLabel: [{
807
+ type: Input
808
+ }], createOption: [{
809
+ type: Input
810
+ }], opened: [{
811
+ type: Output
812
+ }], queryChange: [{
813
+ type: Output
814
+ }], endReached: [{
815
+ type: Output
816
+ }], selected: [{
817
+ type: Output
818
+ }] } });
819
+ /** MM/DD/YYYY masked date input. Emits only parseable ISO dates (or "" when
820
+ * cleared) and reformats the display once a complete date round-trips, exactly
821
+ * like the React package's TextDateInput. */
822
+ class MindBillDateInputComponent {
823
+ value;
824
+ disabled = false;
825
+ ariaLabel = "";
826
+ valueChange = new EventEmitter();
827
+ display = "";
828
+ invalid = false;
829
+ ngOnChanges(changes) {
830
+ if (changes["value"]) {
831
+ this.display = formatMindBillSubmissionDate(this.value);
832
+ this.invalid = false;
833
+ }
834
+ }
835
+ onInput(event) {
836
+ const next = event.target.value;
837
+ const parsed = parseMindBillSubmissionDate(next);
838
+ this.display = next;
839
+ this.invalid = Boolean(next && !parsed);
840
+ if (!next.trim())
841
+ this.valueChange.emit("");
842
+ else if (parsed)
843
+ this.valueChange.emit(parsed);
844
+ }
845
+ onBlur() {
846
+ this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
847
+ }
848
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
849
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillDateInputComponent, isStandalone: true, selector: "mindbill-date-input", inputs: { value: "value", disabled: "disabled", ariaLabel: "ariaLabel" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
850
+ <input
851
+ type="text"
852
+ inputmode="numeric"
853
+ autocomplete="off"
854
+ placeholder="MM/DD/YYYY"
855
+ [attr.aria-label]="ariaLabel"
856
+ [attr.aria-invalid]="invalid || null"
857
+ [disabled]="disabled"
858
+ [value]="display"
859
+ (input)="onInput($event)"
860
+ (blur)="onBlur()"
861
+ />
862
+ @if (invalid) { <small>Use MM/DD/YYYY</small> }
863
+ `, isInline: true, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
864
+ }
865
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
866
+ type: Component,
867
+ args: [{ selector: "mindbill-date-input", standalone: true, imports: [CommonModule], template: `
868
+ <input
869
+ type="text"
870
+ inputmode="numeric"
871
+ autocomplete="off"
872
+ placeholder="MM/DD/YYYY"
873
+ [attr.aria-label]="ariaLabel"
874
+ [attr.aria-invalid]="invalid || null"
875
+ [disabled]="disabled"
876
+ [value]="display"
877
+ (input)="onInput($event)"
878
+ (blur)="onBlur()"
879
+ />
880
+ @if (invalid) { <small>Use MM/DD/YYYY</small> }
881
+ `, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"] }]
882
+ }], propDecorators: { value: [{
883
+ type: Input
884
+ }], disabled: [{
885
+ type: Input
886
+ }], ariaLabel: [{
887
+ type: Input
888
+ }], valueChange: [{
889
+ type: Output
890
+ }] } });
891
+
892
+ const MAX_PDF_BYTES = 25 * 1024 * 1024;
893
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
894
+ const MAX_DOCUMENTS = 20;
457
895
  const PROCEDURES = [
458
896
  { code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
459
897
  { code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
@@ -577,8 +1015,18 @@ class MindBillBillSubmissionComponent {
577
1015
  diagnosisBusy = false;
578
1016
  submitting = false;
579
1017
  errorMessage = "";
1018
+ postalStatus = "";
1019
+ dragActive = false;
580
1020
  invalidFields = new Set();
581
1021
  quickDiagnoses = QUICK_DIAGNOSES;
1022
+ // Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
1023
+ // must schedule change detection explicitly for zone-less Angular hosts.
1024
+ changeDetector = inject(ChangeDetectorRef);
1025
+ customProcedureOption = mindBillCustomProcedureOption;
1026
+ customTaxonomyOption = (query) => {
1027
+ const code = query.trim().toUpperCase();
1028
+ return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
1029
+ };
582
1030
  searchTimer;
583
1031
  postalTimer;
584
1032
  ngOnChanges(changes) {
@@ -602,23 +1050,56 @@ class MindBillBillSubmissionComponent {
602
1050
  "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
603
1051
  };
604
1052
  }
1053
+ get procedureComboOptions() {
1054
+ return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
1055
+ }
1056
+ get taxonomyComboOptions() {
1057
+ return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
1058
+ }
1059
+ modifierComboOptions(line) {
1060
+ return this.modifierOptions
1061
+ .filter((item) => !(line.modifiers ?? []).includes(item.code))
1062
+ .map((item) => ({ id: item.code, label: `−${item.code}`, detail: item.description }));
1063
+ }
605
1064
  referenceClient() {
606
1065
  return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
607
1066
  }
1067
+ setDate(path, value) {
1068
+ if (path === "patient.dateOfBirth")
1069
+ this.bill.patient.dateOfBirth = value;
1070
+ else if (path === "service.date")
1071
+ this.bill.service.date = value;
1072
+ else
1073
+ this.bill.claim.dateOfInjury = value;
1074
+ if (value)
1075
+ this.invalidFields.delete(path);
1076
+ this.billChange.emit(this.bill);
1077
+ }
608
1078
  postalChanged(value) {
609
1079
  clearTimeout(this.postalTimer);
610
- if (!/^\d{5}(?:-\d{4})?$/.test(value.trim()))
1080
+ this.postalStatus = "";
1081
+ if (!/^\d{5}$/.test(value.trim()))
611
1082
  return;
1083
+ this.postalStatus = "Looking up ZIP…";
612
1084
  this.postalTimer = setTimeout(async () => {
613
1085
  try {
614
1086
  const place = await this.referenceClient().lookupPostalCode(value);
615
1087
  if (place) {
616
1088
  this.bill.patient.address.city = place.city;
617
- this.bill.patient.address.state = place.state;
1089
+ this.bill.patient.address.state = place.state.toUpperCase();
1090
+ this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
1091
+ this.invalidFields.delete("patient.address.city");
1092
+ this.invalidFields.delete("patient.address.state");
618
1093
  this.billChange.emit(this.bill);
619
1094
  }
1095
+ else {
1096
+ this.postalStatus = "ZIP not found";
1097
+ }
620
1098
  }
621
- catch { /* City/state remain editable when reference lookup is unavailable. */ }
1099
+ catch {
1100
+ this.postalStatus = "ZIP lookup unavailable";
1101
+ }
1102
+ this.changeDetector.markForCheck();
622
1103
  }, 180);
623
1104
  }
624
1105
  searchPayers() {
@@ -633,6 +1114,7 @@ class MindBillBillSubmissionComponent {
633
1114
  }
634
1115
  finally {
635
1116
  this.payerBusy = false;
1117
+ this.changeDetector.markForCheck();
636
1118
  }
637
1119
  }, 180);
638
1120
  }
@@ -664,6 +1146,7 @@ class MindBillBillSubmissionComponent {
664
1146
  }
665
1147
  finally {
666
1148
  this.diagnosisBusy = false;
1149
+ this.changeDetector.markForCheck();
667
1150
  }
668
1151
  }, reset ? 160 : 0);
669
1152
  }
@@ -690,15 +1173,96 @@ class MindBillBillSubmissionComponent {
690
1173
  }
691
1174
  removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
692
1175
  this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
693
- setModifier(index, value) { this.bill.serviceLines[index].modifiers = value ? [value] : []; this.billChange.emit(this.bill); }
1176
+ selectProcedure(index, option) {
1177
+ this.bill.serviceLines[index].code = option.id;
1178
+ this.invalidFields.delete("serviceLines");
1179
+ this.ensureTrailingLine();
1180
+ }
1181
+ addModifier(index, option) {
1182
+ const line = this.bill.serviceLines[index];
1183
+ line.modifiers = [...new Set([...(line.modifiers ?? []), option.id])];
1184
+ this.billChange.emit(this.bill);
1185
+ }
1186
+ removeModifier(index, modifier) {
1187
+ const line = this.bill.serviceLines[index];
1188
+ line.modifiers = (line.modifiers ?? []).filter((item) => item !== modifier);
1189
+ this.billChange.emit(this.bill);
1190
+ }
1191
+ dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
1192
+ hasDiagnosisPointer(line, pointer) {
1193
+ return (line.diagnosisPointers ?? []).includes(pointer);
1194
+ }
1195
+ toggleDiagnosisPointer(index, pointer) {
1196
+ const line = this.bill.serviceLines[index];
1197
+ line.diagnosisPointers = this.hasDiagnosisPointer(line, pointer)
1198
+ ? (line.diagnosisPointers ?? []).filter((item) => item !== pointer)
1199
+ : [...(line.diagnosisPointers ?? []), pointer].sort((left, right) => left - right);
1200
+ this.billChange.emit(this.bill);
1201
+ }
1202
+ setAuthorizationNumber(value) {
1203
+ this.bill.service.authorizationNumber = value.trim() ? value : null;
1204
+ this.billChange.emit(this.bill);
1205
+ }
1206
+ selectTaxonomy(option) {
1207
+ this.bill.renderingProvider.taxonomy = option.id;
1208
+ this.invalidFields.delete("renderingProvider.taxonomy");
1209
+ this.billChange.emit(this.bill);
1210
+ }
694
1211
  procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
695
1212
  allowed(line) { const base = this.procedureOptions.find((item) => item.code.toLowerCase() === line.code.trim().toLowerCase())?.allowedAmount; return base == null ? (line.charge ?? null) : Math.round(base * Math.max(1, line.units ?? 1) * 100) / 100; }
696
1213
  get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
697
- async filesSelected(event) {
698
- const input = event.target;
699
- const files = Array.from(input.files ?? []).filter((file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf"));
1214
+ onWindowDragOver(event) {
1215
+ if (event.dataTransfer?.types.includes("Files")) {
1216
+ event.preventDefault();
1217
+ this.dragActive = true;
1218
+ this.changeDetector.markForCheck();
1219
+ }
1220
+ }
1221
+ onWindowDragLeave(event) {
1222
+ if (!event.relatedTarget) {
1223
+ this.dragActive = false;
1224
+ this.changeDetector.markForCheck();
1225
+ }
1226
+ }
1227
+ onWindowDrop(event) {
1228
+ if (event.dataTransfer?.files.length) {
1229
+ event.preventDefault();
1230
+ this.dragActive = false;
1231
+ void this.addFiles(event.dataTransfer.files);
1232
+ this.changeDetector.markForCheck();
1233
+ }
1234
+ }
1235
+ async addFiles(fileList) {
1236
+ const files = Array.from(fileList);
1237
+ if (!files.length)
1238
+ return;
1239
+ this.errorMessage = "";
1240
+ const invalid = files.find((file) => file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf"));
1241
+ if (invalid) {
1242
+ this.errorMessage = `${invalid.name} is not a PDF.`;
1243
+ return;
1244
+ }
1245
+ const oversized = files.find((file) => file.size > MAX_PDF_BYTES);
1246
+ if (oversized) {
1247
+ this.errorMessage = `${oversized.name} is larger than 25 MB.`;
1248
+ return;
1249
+ }
1250
+ if (this.workingAttachments.length + files.length > MAX_DOCUMENTS) {
1251
+ this.errorMessage = `A bill can include at most ${MAX_DOCUMENTS} attachments.`;
1252
+ return;
1253
+ }
1254
+ const existingBytes = this.workingAttachments.reduce((sum, item) => sum + (item.contentBase64 ? item.contentBase64.length * 0.75 : 0), 0);
1255
+ if (existingBytes + files.reduce((sum, file) => sum + file.size, 0) > MAX_UPLOAD_BYTES) {
1256
+ this.errorMessage = "Attachments exceed the 100 MB upload limit.";
1257
+ return;
1258
+ }
700
1259
  for (const file of files)
701
1260
  this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
1261
+ this.changeDetector.markForCheck();
1262
+ }
1263
+ async filesSelected(event) {
1264
+ const input = event.target;
1265
+ await this.addFiles(input.files ?? []);
702
1266
  input.value = "";
703
1267
  }
704
1268
  removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
@@ -710,9 +1274,9 @@ class MindBillBillSubmissionComponent {
710
1274
  bad(path) { return this.invalidFields.has(path); }
711
1275
  validate() {
712
1276
  const candidates = [
713
- ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", this.bill.patient.dateOfBirth],
1277
+ ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
714
1278
  ["patient.address.line1", this.bill.patient.address.line1], ["patient.address.city", this.bill.patient.address.city], ["patient.address.state", this.bill.patient.address.state], ["patient.address.postalCode", this.bill.patient.address.postalCode],
715
- ["service.date", this.bill.service.date], ["claim.dateOfInjury", this.bill.claim.dateOfInjury], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
1279
+ ["service.date", parseMindBillSubmissionDate(this.bill.service.date ?? "")], ["claim.dateOfInjury", parseMindBillSubmissionDate(this.bill.claim.dateOfInjury ?? "")], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
716
1280
  ["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
717
1281
  ["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
718
1282
  ["billingProvider.address.line1", this.bill.billingProvider.address.line1], ["billingProvider.address.city", this.bill.billingProvider.address.city], ["billingProvider.address.state", this.bill.billingProvider.address.state], ["billingProvider.address.postalCode", this.bill.billingProvider.address.postalCode],
@@ -759,6 +1323,9 @@ class MindBillBillSubmissionComponent {
759
1323
  this.submitting = true;
760
1324
  try {
761
1325
  const bill = deepCopyBill(this.bill);
1326
+ bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
1327
+ bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
1328
+ bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
762
1329
  bill.serviceLines = bill.serviceLines
763
1330
  .filter((line) => line.code.trim())
764
1331
  .map((line) => {
@@ -778,27 +1345,29 @@ class MindBillBillSubmissionComponent {
778
1345
  }
779
1346
  finally {
780
1347
  this.submitting = false;
1348
+ this.changeDetector.markForCheck();
781
1349
  }
782
1350
  }
783
1351
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
784
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillSubmissionComponent, isStandalone: true, selector: "mindbill-bill-submission", inputs: { initialBill: "initialBill", attachments: "attachments", appearance: "appearance", sessionEndpoint: "sessionEndpoint", getSession: "getSession", apiBaseUrl: "apiBaseUrl", heading: "heading", description: "description", submitLabel: "submitLabel", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, usesOnChanges: true, ngImport: i0, template: `
1352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillSubmissionComponent, isStandalone: true, selector: "mindbill-bill-submission", inputs: { initialBill: "initialBill", attachments: "attachments", appearance: "appearance", sessionEndpoint: "sessionEndpoint", getSession: "getSession", apiBaseUrl: "apiBaseUrl", heading: "heading", description: "description", submitLabel: "submitLabel", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, host: { listeners: { "window:dragover": "onWindowDragOver($event)", "window:dragleave": "onWindowDragLeave($event)", "window:drop": "onWindowDrop($event)" } }, usesOnChanges: true, ngImport: i0, template: `
785
1353
  <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
786
1354
  <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
787
1355
 
788
1356
  <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
789
1357
  <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
790
1358
  <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
791
- <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><input name="dob" [(ngModel)]="bill.patient.dateOfBirth" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
792
- <label>Phone (optional)<input name="phone" [(ngModel)]="bill.patient.phone"></label>
1359
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
1360
+ <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
793
1361
  <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
794
- <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric"></label>
1362
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
795
1363
  <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
796
1364
  <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
797
1365
  </div></fieldset>
798
1366
 
799
1367
  <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
800
- <label [class.invalid]="bad('service.date')">Date of service <b>*</b><input name="dos" [(ngModel)]="bill.service.date" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
801
- <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input name="doi" [(ngModel)]="bill.claim.dateOfInjury" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
1368
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
1369
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
1370
+ <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
802
1371
  <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
803
1372
  <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
804
1373
  <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
@@ -812,6 +1381,7 @@ class MindBillBillSubmissionComponent {
812
1381
  <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
813
1382
  @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
814
1383
  <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1384
+ @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
815
1385
  @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
816
1386
  </div>
817
1387
  </div></fieldset>
@@ -820,67 +1390,86 @@ class MindBillBillSubmissionComponent {
820
1390
  <h3 class="wide">Billing provider</h3>
821
1391
  <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
822
1392
  <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
823
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric"></label>
824
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" [(ngModel)]="bill.billingProvider.phone"></label>
1393
+ <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1394
+ <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
825
1395
  <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
826
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode"></label>
1396
+ <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
827
1397
  <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
828
1398
  <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
829
1399
  <h3 class="wide">Rendering provider</h3>
830
1400
  <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
831
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric"></label>
832
- <label class="wide" [class.invalid]="bad('renderingProvider.taxonomy')">Rendering taxonomy <b>*</b><input name="taxonomy" [(ngModel)]="bill.renderingProvider.taxonomy" list="mb-taxonomies" placeholder="Search by code or specialty"><datalist id="mb-taxonomies">@for (item of taxonomyOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist></label>
1401
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1402
+ <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1403
+ <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
1404
+ <small>Search by specialty name or 10-character taxonomy code.</small>
1405
+ </div>
833
1406
  <h3 class="wide">Place of service</h3>
834
- <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11"></label>
1407
+ <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
835
1408
  <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
836
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode"></label>
1409
+ <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
837
1410
  <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
838
1411
  <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
839
1412
  </div></fieldset>
840
1413
 
841
1414
  <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
842
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Units *</span><span>Allowed</span><span></span></div>
1415
+ <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
843
1416
  @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
844
- <label><span class="mobile-label">Procedure</span><input [name]="'code'+index" [(ngModel)]="line.code" (ngModelChange)="ensureTrailingLine()" list="mb-procedures" placeholder="Code…"><small>{{ procedureDescription(line.code) }}</small></label>
845
- <label><span class="mobile-label">Modifiers</span><select [name]="'modifier'+index" [ngModel]="line.modifiers?.[0] || ''" (ngModelChange)="setModifier(index,$event)"><option value="">Add modifier…</option>@for (item of modifierOptions; track item.code) { <option [value]="item.code">–{{ item.code }} · {{ item.description }}</option> }</select></label>
1417
+ <div class="cell"><span class="mobile-label">Procedure</span>
1418
+ <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1419
+ @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1420
+ </div>
1421
+ <div class="cell"><span class="mobile-label">Modifiers</span>
1422
+ @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1423
+ <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
1424
+ </div>
1425
+ <div class="cell"><span class="mobile-label">Dx pointers</span>
1426
+ <div class="dx">
1427
+ @if (bill.diagnoses.length) {
1428
+ @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1429
+ <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
1430
+ }
1431
+ } @else { <small>Add diagnoses above</small> }
1432
+ </div>
1433
+ </div>
846
1434
  <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
847
1435
  <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
848
1436
  <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
849
1437
  </div> }
850
- <datalist id="mb-procedures">@for (item of procedureOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist>
851
1438
  <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
852
1439
  </fieldset>
853
1440
 
854
1441
  <fieldset><legend>Attachments</legend>
855
1442
  @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
856
- <label class="dropzone"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>PDF supporting documents are sent with this immutable submission.</span></label>
1443
+ <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
857
1444
  </fieldset>
858
1445
 
859
1446
  @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
860
1447
  <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1448
+ @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
861
1449
  </form>
862
- `, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.line-head,.service-line{display:grid;grid-template-columns:1.7fr 1.5fr .55fr .75fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label{gap:4px}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
1450
+ `, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MindBillComboBoxComponent, selector: "mindbill-combo-box", inputs: ["value", "placeholder", "options", "disabled", "loading", "loadingMore", "invalid", "preserveValueOnOpen", "ariaLabel", "createOption"], outputs: ["opened", "queryChange", "endReached", "selected"] }, { kind: "component", type: MindBillDateInputComponent, selector: "mindbill-date-input", inputs: ["value", "disabled", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
863
1451
  }
864
1452
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
865
1453
  type: Component,
866
- args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule], template: `
1454
+ args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule, MindBillComboBoxComponent, MindBillDateInputComponent], template: `
867
1455
  <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
868
1456
  <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
869
1457
 
870
1458
  <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
871
1459
  <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
872
1460
  <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
873
- <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><input name="dob" [(ngModel)]="bill.patient.dateOfBirth" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
874
- <label>Phone (optional)<input name="phone" [(ngModel)]="bill.patient.phone"></label>
1461
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
1462
+ <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
875
1463
  <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
876
- <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric"></label>
1464
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
877
1465
  <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
878
1466
  <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
879
1467
  </div></fieldset>
880
1468
 
881
1469
  <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
882
- <label [class.invalid]="bad('service.date')">Date of service <b>*</b><input name="dos" [(ngModel)]="bill.service.date" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
883
- <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input name="doi" [(ngModel)]="bill.claim.dateOfInjury" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
1470
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
1471
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
1472
+ <label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
884
1473
  <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
885
1474
  <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
886
1475
  <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
@@ -894,6 +1483,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
894
1483
  <div class="quick">@for (item of quickDiagnoses; track item.code) { <button type="button" [class.on]="hasDiagnosis(item.code)" (click)="toggleDiagnosis(item)">{{ hasDiagnosis(item.code) ? '✓' : '+' }} {{ item.label }}</button> }</div>
895
1484
  @if (bill.diagnoses.length) { <div class="chips">@for (code of bill.diagnoses; track code) { <button type="button" (click)="removeDiagnosis(code)"><strong>{{ code }}</strong> ×</button> }</div> }
896
1485
  <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1486
+ @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
897
1487
  @if (diagnosisResults.length) { <div class="options diagnoses" (scroll)="diagnosisScrolled($event)">@for (item of diagnosisResults; track item.code) { <button type="button" (click)="addDiagnosis(item)"><strong>{{ item.code }}</strong><span>{{ item.description }}</span></button> }</div> }
898
1488
  </div>
899
1489
  </div></fieldset>
@@ -902,46 +1492,64 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
902
1492
  <h3 class="wide">Billing provider</h3>
903
1493
  <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
904
1494
  <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
905
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric"></label>
906
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" [(ngModel)]="bill.billingProvider.phone"></label>
1495
+ <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1496
+ <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
907
1497
  <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
908
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode"></label>
1498
+ <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
909
1499
  <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
910
1500
  <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
911
1501
  <h3 class="wide">Rendering provider</h3>
912
1502
  <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
913
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric"></label>
914
- <label class="wide" [class.invalid]="bad('renderingProvider.taxonomy')">Rendering taxonomy <b>*</b><input name="taxonomy" [(ngModel)]="bill.renderingProvider.taxonomy" list="mb-taxonomies" placeholder="Search by code or specialty"><datalist id="mb-taxonomies">@for (item of taxonomyOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist></label>
1503
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1504
+ <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1505
+ <mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
1506
+ <small>Search by specialty name or 10-character taxonomy code.</small>
1507
+ </div>
915
1508
  <h3 class="wide">Place of service</h3>
916
- <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11"></label>
1509
+ <label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
917
1510
  <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
918
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode"></label>
1511
+ <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
919
1512
  <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
920
1513
  <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
921
1514
  </div></fieldset>
922
1515
 
923
1516
  <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
924
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Units *</span><span>Allowed</span><span></span></div>
1517
+ <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
925
1518
  @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
926
- <label><span class="mobile-label">Procedure</span><input [name]="'code'+index" [(ngModel)]="line.code" (ngModelChange)="ensureTrailingLine()" list="mb-procedures" placeholder="Code…"><small>{{ procedureDescription(line.code) }}</small></label>
927
- <label><span class="mobile-label">Modifiers</span><select [name]="'modifier'+index" [ngModel]="line.modifiers?.[0] || ''" (ngModelChange)="setModifier(index,$event)"><option value="">Add modifier…</option>@for (item of modifierOptions; track item.code) { <option [value]="item.code">–{{ item.code }} · {{ item.description }}</option> }</select></label>
1519
+ <div class="cell"><span class="mobile-label">Procedure</span>
1520
+ <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1521
+ @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1522
+ </div>
1523
+ <div class="cell"><span class="mobile-label">Modifiers</span>
1524
+ @if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
1525
+ <mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
1526
+ </div>
1527
+ <div class="cell"><span class="mobile-label">Dx pointers</span>
1528
+ <div class="dx">
1529
+ @if (bill.diagnoses.length) {
1530
+ @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1531
+ <button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
1532
+ }
1533
+ } @else { <small>Add diagnoses above</small> }
1534
+ </div>
1535
+ </div>
928
1536
  <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
929
1537
  <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
930
1538
  <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
931
1539
  </div> }
932
- <datalist id="mb-procedures">@for (item of procedureOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist>
933
1540
  <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
934
1541
  </fieldset>
935
1542
 
936
1543
  <fieldset><legend>Attachments</legend>
937
1544
  @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
938
- <label class="dropzone"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>PDF supporting documents are sent with this immutable submission.</span></label>
1545
+ <label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
939
1546
  </fieldset>
940
1547
 
941
1548
  @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
942
1549
  <div class="submit-row"><span>{{ invalidFields.size ? 'Complete the highlighted required fields.' : 'All required billing data is ready.' }}</span><button type="submit" [disabled]="submitting">{{ submitting ? 'Submitting…' : submitLabel }}</button></div>
1550
+ @if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
943
1551
  </form>
944
- `, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.line-head,.service-line{display:grid;grid-template-columns:1.7fr 1.5fr .55fr .75fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label{gap:4px}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
1552
+ `, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
945
1553
  }], propDecorators: { initialBill: [{
946
1554
  type: Input
947
1555
  }], attachments: [{
@@ -974,6 +1582,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
974
1582
  type: Output
975
1583
  }], billChange: [{
976
1584
  type: Output
1585
+ }], onWindowDragOver: [{
1586
+ type: HostListener,
1587
+ args: ["window:dragover", ["$event"]]
1588
+ }], onWindowDragLeave: [{
1589
+ type: HostListener,
1590
+ args: ["window:dragleave", ["$event"]]
1591
+ }], onWindowDrop: [{
1592
+ type: HostListener,
1593
+ args: ["window:drop", ["$event"]]
977
1594
  }] } });
978
1595
 
979
1596
  const THEME = {
@@ -992,6 +1609,7 @@ class MindBillBillingManagementButtonComponent {
992
1609
  failed = new EventEmitter();
993
1610
  loading = false;
994
1611
  errorMessage = "";
1612
+ changeDetector = inject(ChangeDetectorRef);
995
1613
  get themeStyle() {
996
1614
  const base = THEME[this.appearance.preset ?? "mindbill"] ?? THEME["mindbill"];
997
1615
  return {
@@ -1024,10 +1642,12 @@ class MindBillBillingManagementButtonComponent {
1024
1642
  catch (error) {
1025
1643
  popup?.close();
1026
1644
  this.errorMessage = error instanceof Error ? error.message : "Billing management could not be opened.";
1645
+ this.changeDetector.markForCheck();
1027
1646
  this.failed.emit(error);
1028
1647
  }
1029
1648
  finally {
1030
1649
  this.loading = false;
1650
+ this.changeDetector.markForCheck();
1031
1651
  }
1032
1652
  }
1033
1653
  async fetchSession() {
@@ -1084,5 +1704,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1084
1704
  * Generated bundle index. Do not edit.
1085
1705
  */
1086
1706
 
1087
- export { MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillSubmissionComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillLifecycleStore, buildMindBillReportCsv, buildMindBillReportRows, ensureTrailingProcedureLine, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, summarizeMindBillDashboard };
1707
+ export { MINDBILL_STATUS_AGING_BUCKETS, MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillSubmissionComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillComboBoxComponent, MindBillDateInputComponent, MindBillLifecycleStore, MindBillStatusAgingMatrixComponent, buildMindBillReportCsv, buildMindBillReportRows, buildMindBillStatusAgingCsv, buildMindBillStatusAgingMatrix, ensureTrailingProcedureLine, formatMindBillSubmissionDate, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, mindBillCustomProcedureOption, parseMindBillSubmissionDate, summarizeMindBillDashboard };
1088
1708
  //# sourceMappingURL=mindbill-angular.mjs.map