@mindbill/angular 0.6.1 → 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.
- package/README.md +12 -2
- package/fesm2022/mindbill-angular.mjs +671 -51
- package/fesm2022/mindbill-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mindbill-angular.d.ts +136 -3
|
@@ -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$
|
|
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$
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
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"
|
|
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 & claim</legend><div class="grid">
|
|
800
|
-
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><input
|
|
801
|
-
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input
|
|
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
|
-
<
|
|
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
|
-
<
|
|
845
|
-
|
|
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>
|
|
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}.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>
|
|
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
|
|
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"
|
|
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 & claim</legend><div class="grid">
|
|
882
|
-
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><input
|
|
883
|
-
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input
|
|
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
|
-
<
|
|
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
|
-
<
|
|
927
|
-
|
|
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>
|
|
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}.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>
|
|
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
|