@mindbill/angular 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, computed, EventEmitter, effect, Output, Input, Component, Directive } from '@angular/core';
3
- import { createBillLifecycleClient } from '@mindbill/browser';
3
+ import { createBillLifecycleClient, createBillReferenceClient, createBillSubmissionClient } from '@mindbill/browser';
4
4
  import * as i1 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import * as i2 from '@angular/forms';
@@ -275,7 +275,7 @@ function ensureTrailingProcedureLine(lines) {
275
275
  : entered;
276
276
  }
277
277
 
278
- const THEME = {
278
+ const THEME$2 = {
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" },
@@ -316,10 +316,18 @@ function summarizeMindBillDashboard(bills, now = new Date()) {
316
316
  totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
317
317
  outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
318
318
  openCount: bills.filter((item) => !/closed|paid|written.off/i.test(item.state)).length,
319
+ submittedThisMonth: bills.filter((item) => sameMonth(item.submittedAt, now)).length,
320
+ closedThisMonth: bills.filter((item) => /closed|paid|written.off/i.test(item.state) && sameMonth(item.updatedAt ?? item.submittedAt, now)).length,
319
321
  bills,
320
322
  aging,
321
323
  };
322
324
  }
325
+ function sameMonth(value, now) {
326
+ if (!value)
327
+ return false;
328
+ const date = new Date(value);
329
+ return Number.isFinite(date.getTime()) && date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth();
330
+ }
323
331
  function buildMindBillReportRows(bills, dimension) {
324
332
  const rows = new Map();
325
333
  for (const bill of bills) {
@@ -341,7 +349,7 @@ function buildMindBillReportCsv(rows) {
341
349
  class DashboardBase {
342
350
  appearance = { preset: "mindbill" };
343
351
  get themeStyle() {
344
- const base = THEME[this.appearance.preset ?? "mindbill"] ?? THEME["mindbill"];
352
+ const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
345
353
  return {
346
354
  "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
347
355
  "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
@@ -364,15 +372,21 @@ const sharedStyles = `
364
372
  `;
365
373
  class MindBillBillAgingSummaryComponent extends DashboardBase {
366
374
  buckets = [];
375
+ selectedBucket = "";
376
+ bucketSelected = new EventEmitter();
367
377
  width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
368
378
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
369
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillAgingSummaryComponent, isStandalone: true, selector: "mindbill-bill-aging-summary", inputs: { buckets: "buckets" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <article class="card"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></article> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging article{padding:16px}.aging article>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
379
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillAgingSummaryComponent, isStandalone: true, selector: "mindbill-bill-aging-summary", inputs: { buckets: "buckets", selectedBucket: "selectedBucket" }, outputs: { bucketSelected: "bucketSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
370
380
  }
371
381
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
372
382
  type: Component,
373
- args: [{ selector: "mindbill-bill-aging-summary", standalone: true, imports: [CommonModule], template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <article class="card"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></article> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging article{padding:16px}.aging article>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"] }]
383
+ args: [{ selector: "mindbill-bill-aging-summary", standalone: true, imports: [CommonModule], template: `<section class="mbd aging" [ngStyle]="themeStyle" aria-label="Accounts receivable aging">@for (bucket of buckets; track bucket.id) { <button type="button" class="card" [class.selected]="selectedBucket === bucket.id" (click)="bucketSelected.emit(bucket.id)"><div><strong>{{ bucket.label }}</strong><span>{{ bucket.count }} bill{{ bucket.count === 1 ? '' : 's' }}</span></div><b class="money">{{ bucket.balance | currency }}</b><i><em [style.width.%]="width(bucket.balance)"></em></i></button> }</section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".aging{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.aging>button{border:1px solid var(--b);padding:16px;text-align:left;color:var(--t);cursor:pointer}.aging>button:hover,.aging>button.selected{border-color:var(--a);background:color-mix(in srgb,var(--a) 6%,var(--s))}.aging button>div{display:flex;justify-content:space-between;gap:8px}.aging span{color:var(--m);font-size:12px}.aging b{display:block;font-size:20px;margin:14px 0 12px}.aging i{display:block;height:5px;border-radius:99px;background:color-mix(in srgb,var(--b) 70%,transparent);overflow:hidden}.aging em{display:block;height:100%;background:var(--a)}@media(max-width:760px){.aging{grid-template-columns:1fr 1fr}}\n"] }]
374
384
  }], propDecorators: { buckets: [{
375
385
  type: Input
386
+ }], selectedBucket: [{
387
+ type: Input
388
+ }], bucketSelected: [{
389
+ type: Output
376
390
  }] } });
377
391
  class MindBillBillListComponent extends DashboardBase {
378
392
  bills = [];
@@ -398,16 +412,19 @@ class MindBillBillingDashboardComponent extends DashboardBase {
398
412
  createBill = new EventEmitter();
399
413
  query = "";
400
414
  state = "";
415
+ agingBucket = "";
401
416
  get summary() { return summarizeMindBillDashboard(this.bills); }
402
417
  get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
403
- get filteredBills() { const query = this.query.trim().toLowerCase(); return this.bills.filter((bill) => (!this.state || bill.state === this.state) && (!query || [bill.patientName, bill.claimNumber, bill.payerName, bill.externalId, bill.billNumber].some((value) => String(value ?? "").toLowerCase().includes(query)))); }
418
+ get filteredBills() { const query = this.query.trim().toLowerCase(); return this.bills.filter((bill) => (!this.state || bill.state === this.state) && (!this.agingBucket || mindBillAgingBucket(mindBillAgingDays(bill)) === this.agingBucket) && (!query || [bill.patientName, bill.claimNumber, bill.payerName, bill.externalId, bill.billNumber].some((value) => String(value ?? "").toLowerCase().includes(query)))); }
419
+ get agingLabel() { return this.summary.aging.find((item) => item.id === this.agingBucket)?.label ?? ""; }
420
+ toggleAging(bucket) { this.agingBucket = this.agingBucket === bucket ? "" : bucket; }
404
421
  label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
405
422
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
406
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingDashboardComponent, isStandalone: true, selector: "mindbill-billing-dashboard", inputs: { bills: "bills", heading: "heading", description: "description" }, outputs: { billSelected: "billSelected", createBill: "createBill" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Collected</span><b>{{ summary.totalPaid | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Open bills</span><b>{{ summary.openCount }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [appearance]="appearance"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select></div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px;gap:10px}.filters input,.filters select{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MindBillBillAgingSummaryComponent, selector: "mindbill-bill-aging-summary", inputs: ["buckets"] }, { kind: "component", type: MindBillBillListComponent, selector: "mindbill-bill-list", inputs: ["bills"], outputs: ["billSelected"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
423
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingDashboardComponent, isStandalone: true, selector: "mindbill-billing-dashboard", inputs: { bills: "bills", heading: "heading", description: "description" }, outputs: { billSelected: "billSelected", createBill: "createBill" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, isInline: true, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MindBillBillAgingSummaryComponent, selector: "mindbill-bill-aging-summary", inputs: ["buckets", "selectedBucket"], outputs: ["bucketSelected"] }, { kind: "component", type: MindBillBillListComponent, selector: "mindbill-bill-list", inputs: ["bills"], outputs: ["billSelected"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
407
424
  }
408
425
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
409
426
  type: Component,
410
- args: [{ selector: "mindbill-billing-dashboard", standalone: true, imports: [CommonModule, FormsModule, MindBillBillAgingSummaryComponent, MindBillBillListComponent], template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Collected</span><b>{{ summary.totalPaid | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Open bills</span><b>{{ summary.openCount }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [appearance]="appearance"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select></div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px;gap:10px}.filters input,.filters select{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"] }]
427
+ args: [{ selector: "mindbill-billing-dashboard", standalone: true, imports: [CommonModule, FormsModule, MindBillBillAgingSummaryComponent, MindBillBillListComponent], template: `<section class="mbd dashboard" [ngStyle]="themeStyle"><header><div><p>Billing operations</p><h2>{{ heading }}</h2><span class="muted">{{ description }}</span></div><button type="button" (click)="createBill.emit()">+ New bill</button></header><div class="metrics"><article class="card"><span>Total billed</span><b>{{ summary.totalBilled | currency }}</b></article><article class="card"><span>Outstanding</span><b>{{ summary.outstanding | currency }}</b></article><article class="card"><span>Submitted this month</span><b>{{ summary.submittedThisMonth }}</b></article><article class="card"><span>Closed this month</span><b>{{ summary.closedThisMonth }}</b></article></div><mindbill-bill-aging-summary [buckets]="summary.aging" [selectedBucket]="agingBucket" [appearance]="appearance" (bucketSelected)="toggleAging($event)"/><div class="filters"><input [(ngModel)]="query" placeholder="Search patient, claim, or payer" aria-label="Search bills"><select [(ngModel)]="state"><option value="">All statuses</option>@for (option of states; track option) { <option [value]="option">{{ label(option) }}</option> }</select>@if (agingBucket) { <button type="button" class="clear" (click)="agingBucket=''">{{ agingLabel }} ×</button> }</div><mindbill-bill-list [bills]="filteredBills" [appearance]="appearance" (billSelected)="billSelected.emit($event)"/></section>`, styles: [":host{display:block}.mbd{font-family:var(--font);color:var(--t)}button,input,select{font:inherit}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r)}.money{font-variant-numeric:tabular-nums}.muted{color:var(--m)}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-weight:750;padding:5px 9px}\n", ".dashboard{display:grid;gap:18px}.dashboard header{display:flex;justify-content:space-between;align-items:end;gap:20px}.dashboard h2{font-size:28px;margin:2px 0 5px}.dashboard header p{margin:0;color:var(--a);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}.dashboard header button{border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:11px 16px;font-weight:750;cursor:pointer}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.metrics article{padding:18px}.metrics span{display:block;color:var(--m);font-size:12px}.metrics b{display:block;margin-top:8px;font-size:24px}.filters{display:grid;grid-template-columns:1fr 190px auto;gap:10px}.filters input,.filters select,.filters .clear{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:11px 13px}.filters .clear{color:var(--a);font-weight:700;cursor:pointer}@media(max-width:760px){.dashboard header{align-items:start}.metrics{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr}}\n"] }]
411
428
  }], propDecorators: { bills: [{
412
429
  type: Input
413
430
  }], heading: [{
@@ -437,9 +454,635 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
437
454
  type: Input
438
455
  }] } });
439
456
 
457
+ const PROCEDURES = [
458
+ { code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
459
+ { code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
460
+ { code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
461
+ { code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
462
+ { code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
463
+ { code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
464
+ { code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
465
+ { code: "90791", description: "Psychiatric diagnostic evaluation" },
466
+ { code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
467
+ { code: "90832", description: "Psychotherapy, 30 minutes" },
468
+ { code: "90834", description: "Psychotherapy, 45 minutes" },
469
+ { code: "90837", description: "Psychotherapy, 60 minutes" },
470
+ { code: "99203", description: "New patient office visit, level 3" },
471
+ { code: "99204", description: "New patient office visit, level 4" },
472
+ { code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
473
+ { code: "99213", description: "Established patient office visit, level 3" },
474
+ { code: "99214", description: "Established patient office visit, level 4" },
475
+ { code: "99215", description: "Established patient office visit, level 5" },
476
+ { code: "96130", description: "Psychological testing evaluation, first hour" },
477
+ { code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
478
+ { code: "97161", description: "Physical therapy evaluation, low complexity" },
479
+ { code: "97162", description: "Physical therapy evaluation, moderate complexity" },
480
+ { code: "97163", description: "Physical therapy evaluation, high complexity" },
481
+ { code: "97110", description: "Therapeutic exercises" },
482
+ { code: "97140", description: "Manual therapy techniques" },
483
+ { code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
484
+ { code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
485
+ { code: "72141", description: "MRI cervical spine without contrast" },
486
+ { code: "72148", description: "MRI lumbar spine without contrast" },
487
+ { code: "73721", description: "MRI lower-extremity joint without contrast" },
488
+ { code: "20610", description: "Major joint injection or aspiration" },
489
+ { code: "99070", description: "Supplies and materials" },
490
+ ];
491
+ const MODIFIERS = [
492
+ { code: "92", description: "Primary Treating Physician evaluation" },
493
+ { code: "93", description: "Interpreter required" },
494
+ { code: "94", description: "Agreed Medical Evaluator" },
495
+ { code: "95", description: "Qualified Medical Evaluator" },
496
+ { code: "96", description: "Psychiatric/psychological evaluation" },
497
+ { code: "97", description: "Toxicology evaluation" },
498
+ { code: "98", description: "Oncology evaluation" },
499
+ { code: "59", description: "Distinct procedural service" },
500
+ { code: "XE", description: "Separate encounter" },
501
+ { code: "XS", description: "Separate structure" },
502
+ { code: "XP", description: "Separate practitioner" },
503
+ { code: "XU", description: "Unusual non-overlapping service" },
504
+ ];
505
+ const TAXONOMIES = [
506
+ { code: "103G00000X", description: "Clinical Neuropsychologist" },
507
+ { code: "103T00000X", description: "Psychologist" },
508
+ { code: "111N00000X", description: "Chiropractor" },
509
+ { code: "207Q00000X", description: "Family Medicine" },
510
+ { code: "207R00000X", description: "Internal Medicine" },
511
+ { code: "207X00000X", description: "Orthopaedic Surgery" },
512
+ { code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
513
+ { code: "208100000X", description: "Physical Medicine & Rehabilitation" },
514
+ { code: "2083X0100X", description: "Occupational Medicine" },
515
+ { code: "2084N0400X", description: "Neurology" },
516
+ { code: "2084P0800X", description: "Psychiatry" },
517
+ { code: "208VP0000X", description: "Pain Medicine" },
518
+ { code: "213E00000X", description: "Podiatrist" },
519
+ { code: "225100000X", description: "Physical Therapist" },
520
+ { code: "225X00000X", description: "Occupational Therapist" },
521
+ { code: "363A00000X", description: "Physician Assistant" },
522
+ { code: "363L00000X", description: "Nurse Practitioner" },
523
+ ];
524
+ const QUICK_DIAGNOSES = [
525
+ { label: "Psych", code: "Z04.6", description: "General psychiatric examination requested by authority" },
526
+ { label: "Back", code: "M54.50", description: "Low back pain, unspecified" },
527
+ { label: "Neck", code: "M54.2", description: "Cervicalgia" },
528
+ { label: "Left hand", code: "M79.642", description: "Pain in left hand" },
529
+ { label: "Right hand", code: "M79.641", description: "Pain in right hand" },
530
+ { label: "Left knee", code: "M25.562", description: "Pain in left knee" },
531
+ { label: "Right knee", code: "M25.561", description: "Pain in right knee" },
532
+ ];
533
+ const THEME$1 = {
534
+ 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" },
535
+ "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" },
536
+ "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" },
537
+ "clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#fff", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
538
+ };
539
+ function deepCopyBill(value) {
540
+ return JSON.parse(JSON.stringify(value));
541
+ }
542
+ function required(value) {
543
+ return typeof value === "string" ? value.trim().length > 0 : value != null;
544
+ }
545
+ async function blobToBase64(blob) {
546
+ const buffer = new Uint8Array(await blob.arrayBuffer());
547
+ let binary = "";
548
+ for (const byte of buffer)
549
+ binary += String.fromCharCode(byte);
550
+ return btoa(binary);
551
+ }
552
+ class MindBillBillSubmissionComponent {
553
+ initialBill;
554
+ attachments = [];
555
+ appearance = { preset: "mindbill" };
556
+ sessionEndpoint = "/api/mindbill/session";
557
+ getSession;
558
+ apiBaseUrl;
559
+ heading = "Bill information";
560
+ description = "Review the bill details, add attachments, and submit.";
561
+ submitLabel = "Submit bill";
562
+ procedureOptions = PROCEDURES;
563
+ modifierOptions = MODIFIERS;
564
+ taxonomyOptions = TAXONOMIES;
565
+ submitter;
566
+ submitted = new EventEmitter();
567
+ submissionError = new EventEmitter();
568
+ billChange = new EventEmitter();
569
+ bill = {};
570
+ workingAttachments = [];
571
+ payerQuery = "";
572
+ payerResults = [];
573
+ payerBusy = false;
574
+ diagnosisQuery = "";
575
+ diagnosisResults = [];
576
+ diagnosisOffset = 0;
577
+ diagnosisBusy = false;
578
+ submitting = false;
579
+ errorMessage = "";
580
+ invalidFields = new Set();
581
+ quickDiagnoses = QUICK_DIAGNOSES;
582
+ searchTimer;
583
+ postalTimer;
584
+ ngOnChanges(changes) {
585
+ if (changes["initialBill"] && this.initialBill) {
586
+ this.bill = deepCopyBill(this.initialBill);
587
+ if (!this.bill.serviceLines.length)
588
+ this.bill.serviceLines = [{ code: "", units: 1 }];
589
+ this.ensureTrailingLine();
590
+ this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
591
+ }
592
+ if (changes["attachments"])
593
+ this.workingAttachments = this.attachments.map((item) => ({ ...item }));
594
+ }
595
+ get themeStyle() {
596
+ const base = THEME$1[this.appearance.preset ?? "mindbill"] ?? THEME$1["mindbill"];
597
+ return {
598
+ "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
599
+ "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
600
+ "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
601
+ "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
602
+ "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
603
+ };
604
+ }
605
+ referenceClient() {
606
+ return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
607
+ }
608
+ postalChanged(value) {
609
+ clearTimeout(this.postalTimer);
610
+ if (!/^\d{5}(?:-\d{4})?$/.test(value.trim()))
611
+ return;
612
+ this.postalTimer = setTimeout(async () => {
613
+ try {
614
+ const place = await this.referenceClient().lookupPostalCode(value);
615
+ if (place) {
616
+ this.bill.patient.address.city = place.city;
617
+ this.bill.patient.address.state = place.state;
618
+ this.billChange.emit(this.bill);
619
+ }
620
+ }
621
+ catch { /* City/state remain editable when reference lookup is unavailable. */ }
622
+ }, 180);
623
+ }
624
+ searchPayers() {
625
+ clearTimeout(this.searchTimer);
626
+ this.searchTimer = setTimeout(async () => {
627
+ this.payerBusy = true;
628
+ try {
629
+ this.payerResults = await this.referenceClient().searchClaimsAdministrators(this.payerQuery, this.bill.claim.claimNumber);
630
+ }
631
+ catch {
632
+ this.payerResults = [];
633
+ }
634
+ finally {
635
+ this.payerBusy = false;
636
+ }
637
+ }, 180);
638
+ }
639
+ selectPayer(payer) {
640
+ this.bill.claim.claimsAdministrator = { id: payer.id, name: payer.name };
641
+ this.payerQuery = payer.name;
642
+ this.payerResults = [];
643
+ this.invalidFields.delete("claim.claimsAdministrator");
644
+ this.billChange.emit(this.bill);
645
+ }
646
+ searchDiagnoses(reset) {
647
+ if (this.diagnosisBusy)
648
+ return;
649
+ if (reset) {
650
+ this.diagnosisOffset = 0;
651
+ this.diagnosisResults = [];
652
+ }
653
+ clearTimeout(this.searchTimer);
654
+ this.searchTimer = setTimeout(async () => {
655
+ this.diagnosisBusy = true;
656
+ try {
657
+ const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
658
+ this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
659
+ this.diagnosisOffset = this.diagnosisResults.length;
660
+ }
661
+ catch {
662
+ if (reset)
663
+ this.diagnosisResults = [];
664
+ }
665
+ finally {
666
+ this.diagnosisBusy = false;
667
+ }
668
+ }, reset ? 160 : 0);
669
+ }
670
+ diagnosisScrolled(event) {
671
+ const element = event.currentTarget;
672
+ if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
673
+ this.searchDiagnoses(false);
674
+ }
675
+ hasDiagnosis(code) { return this.bill.diagnoses.includes(code); }
676
+ toggleDiagnosis(item) {
677
+ if (this.hasDiagnosis(item.code))
678
+ this.removeDiagnosis(item.code);
679
+ else
680
+ this.addDiagnosis(item);
681
+ }
682
+ addDiagnosis(item) { if (!this.hasDiagnosis(item.code))
683
+ this.bill.diagnoses.push(item.code); this.diagnosisQuery = ""; this.diagnosisResults = []; this.invalidFields.delete("diagnoses"); this.billChange.emit(this.bill); }
684
+ removeDiagnosis(code) { this.bill.diagnoses = this.bill.diagnoses.filter((item) => item !== code); this.billChange.emit(this.bill); }
685
+ ensureTrailingLine() {
686
+ const last = this.bill.serviceLines.at(-1);
687
+ if (!last || last.code.trim())
688
+ this.bill.serviceLines.push({ code: "", units: 1 });
689
+ this.billChange.emit(this.bill);
690
+ }
691
+ removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
692
+ this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
693
+ setModifier(index, value) { this.bill.serviceLines[index].modifiers = value ? [value] : []; this.billChange.emit(this.bill); }
694
+ procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
695
+ 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
+ get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
697
+ async filesSelected(event) {
698
+ const input = event.target;
699
+ const files = Array.from(input.files ?? []).filter((file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf"));
700
+ for (const file of files)
701
+ this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
702
+ input.value = "";
703
+ }
704
+ removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
705
+ URL.revokeObjectURL(item.contentUrl); this.workingAttachments.splice(index, 1); }
706
+ previewAttachment(attachment) { if (attachment.contentUrl)
707
+ window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
708
+ else if (attachment.contentBase64)
709
+ window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer"); }
710
+ bad(path) { return this.invalidFields.has(path); }
711
+ validate() {
712
+ const candidates = [
713
+ ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", this.bill.patient.dateOfBirth],
714
+ ["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],
716
+ ["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
717
+ ["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
+ ["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],
719
+ ["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
720
+ ["serviceLocation.placeOfServiceCode", this.bill.serviceLocation.placeOfServiceCode], ["serviceLocation.address.line1", this.bill.serviceLocation.address.line1], ["serviceLocation.address.city", this.bill.serviceLocation.address.city], ["serviceLocation.address.state", this.bill.serviceLocation.address.state], ["serviceLocation.address.postalCode", this.bill.serviceLocation.address.postalCode],
721
+ ["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
722
+ ];
723
+ this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
724
+ if (this.invalidFields.size) {
725
+ this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
726
+ setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
727
+ return false;
728
+ }
729
+ return true;
730
+ }
731
+ async submissionDocuments() {
732
+ const documents = [];
733
+ for (const attachment of this.workingAttachments) {
734
+ let contentBase64 = attachment.contentBase64;
735
+ if (!contentBase64 && attachment.contentUrl) {
736
+ const response = await fetch(attachment.contentUrl);
737
+ if (!response.ok)
738
+ throw new Error(`Could not load ${attachment.filename}.`);
739
+ contentBase64 = await blobToBase64(await response.blob());
740
+ }
741
+ if (!contentBase64)
742
+ continue;
743
+ documents.push({
744
+ filename: attachment.filename,
745
+ documentType: attachment.documentType,
746
+ contentBase64,
747
+ ...(attachment.externalId ? { externalId: attachment.externalId } : {}),
748
+ ...(attachment.description ? { description: attachment.description } : {}),
749
+ ...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
750
+ });
751
+ }
752
+ return documents;
753
+ }
754
+ async submit(event) {
755
+ event.preventDefault();
756
+ this.errorMessage = "";
757
+ if (!this.validate())
758
+ return;
759
+ this.submitting = true;
760
+ try {
761
+ const bill = deepCopyBill(this.bill);
762
+ bill.serviceLines = bill.serviceLines
763
+ .filter((line) => line.code.trim())
764
+ .map((line) => {
765
+ const charge = line.charge ?? this.allowed(line);
766
+ return charge == null ? line : { ...line, charge };
767
+ });
768
+ const input = { bill, documents: await this.submissionDocuments() };
769
+ const result = this.submitter
770
+ ? await this.submitter(input)
771
+ : await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
772
+ this.submitted.emit(result);
773
+ }
774
+ catch (value) {
775
+ const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
776
+ this.errorMessage = error.message;
777
+ this.submissionError.emit(error);
778
+ }
779
+ finally {
780
+ this.submitting = false;
781
+ }
782
+ }
783
+ 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: `
785
+ <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
786
+ <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
787
+
788
+ <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
789
+ <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
790
+ <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
791
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><input name="dob" [(ngModel)]="bill.patient.dateOfBirth" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
792
+ <label>Phone (optional)<input name="phone" [(ngModel)]="bill.patient.phone"></label>
793
+ <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
794
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric"></label>
795
+ <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
796
+ <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
797
+ </div></fieldset>
798
+
799
+ <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
800
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><input name="dos" [(ngModel)]="bill.service.date" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
801
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input name="doi" [(ngModel)]="bill.claim.dateOfInjury" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
802
+ <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
803
+ <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
804
+ <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
805
+ <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
806
+ @if (payerBusy) { <small>Searching directory…</small> }
807
+ @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
808
+ @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
809
+ </div>
810
+ <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
811
+ <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
812
+ <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
+ @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
+ <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
815
+ @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
+ </div>
817
+ </div></fieldset>
818
+
819
+ <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
820
+ <h3 class="wide">Billing provider</h3>
821
+ <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
822
+ <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>
825
+ <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>
827
+ <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
828
+ <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
829
+ <h3 class="wide">Rendering provider</h3>
830
+ <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
831
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric"></label>
832
+ <label class="wide" [class.invalid]="bad('renderingProvider.taxonomy')">Rendering taxonomy <b>*</b><input name="taxonomy" [(ngModel)]="bill.renderingProvider.taxonomy" list="mb-taxonomies" placeholder="Search by code or specialty"><datalist id="mb-taxonomies">@for (item of taxonomyOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist></label>
833
+ <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>
835
+ <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>
837
+ <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
838
+ <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
839
+ </div></fieldset>
840
+
841
+ <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>
843
+ @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
844
+ <label><span class="mobile-label">Procedure</span><input [name]="'code'+index" [(ngModel)]="line.code" (ngModelChange)="ensureTrailingLine()" list="mb-procedures" placeholder="Code…"><small>{{ procedureDescription(line.code) }}</small></label>
845
+ <label><span class="mobile-label">Modifiers</span><select [name]="'modifier'+index" [ngModel]="line.modifiers?.[0] || ''" (ngModelChange)="setModifier(index,$event)"><option value="">Add modifier…</option>@for (item of modifierOptions; track item.code) { <option [value]="item.code">–{{ item.code }} · {{ item.description }}</option> }</select></label>
846
+ <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
847
+ <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
848
+ <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
849
+ </div> }
850
+ <datalist id="mb-procedures">@for (item of procedureOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist>
851
+ <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
852
+ </fieldset>
853
+
854
+ <fieldset><legend>Attachments</legend>
855
+ @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
856
+ <label class="dropzone"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>PDF supporting documents are sent with this immutable submission.</span></label>
857
+ </fieldset>
858
+
859
+ @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
860
+ <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>
861
+ </form>
862
+ `, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.line-head,.service-line{display:grid;grid-template-columns:1.7fr 1.5fr .55fr .75fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label{gap:4px}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
863
+ }
864
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
865
+ type: Component,
866
+ args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule], template: `
867
+ <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
868
+ <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
869
+
870
+ <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
871
+ <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
872
+ <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
873
+ <label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><input name="dob" [(ngModel)]="bill.patient.dateOfBirth" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
874
+ <label>Phone (optional)<input name="phone" [(ngModel)]="bill.patient.phone"></label>
875
+ <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
876
+ <label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric"></label>
877
+ <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
878
+ <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
879
+ </div></fieldset>
880
+
881
+ <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
882
+ <label [class.invalid]="bad('service.date')">Date of service <b>*</b><input name="dos" [(ngModel)]="bill.service.date" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
883
+ <label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><input name="doi" [(ngModel)]="bill.claim.dateOfInjury" placeholder="MM/DD/YYYY" inputmode="numeric"></label>
884
+ <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
885
+ <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
886
+ <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
887
+ <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
888
+ @if (payerBusy) { <small>Searching directory…</small> }
889
+ @if (payerResults.length) { <div class="options">@for (payer of payerResults; track payer.id) { <button type="button" (click)="selectPayer(payer)"><strong>{{ payer.name }}</strong><span>{{ payer.hasElectronic ? 'Electronic routing' : 'Paper / fax' }}@if (payer.confidence) { · {{ payer.confidence }} match }</span></button> }</div> }
890
+ @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
891
+ </div>
892
+ <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
893
+ <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
894
+ <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
+ @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
+ <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
897
+ @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
+ </div>
899
+ </div></fieldset>
900
+
901
+ <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
902
+ <h3 class="wide">Billing provider</h3>
903
+ <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
904
+ <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>
907
+ <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>
909
+ <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
910
+ <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
911
+ <h3 class="wide">Rendering provider</h3>
912
+ <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
913
+ <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric"></label>
914
+ <label class="wide" [class.invalid]="bad('renderingProvider.taxonomy')">Rendering taxonomy <b>*</b><input name="taxonomy" [(ngModel)]="bill.renderingProvider.taxonomy" list="mb-taxonomies" placeholder="Search by code or specialty"><datalist id="mb-taxonomies">@for (item of taxonomyOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist></label>
915
+ <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>
917
+ <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>
919
+ <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
920
+ <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
921
+ </div></fieldset>
922
+
923
+ <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>
925
+ @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
926
+ <label><span class="mobile-label">Procedure</span><input [name]="'code'+index" [(ngModel)]="line.code" (ngModelChange)="ensureTrailingLine()" list="mb-procedures" placeholder="Code…"><small>{{ procedureDescription(line.code) }}</small></label>
927
+ <label><span class="mobile-label">Modifiers</span><select [name]="'modifier'+index" [ngModel]="line.modifiers?.[0] || ''" (ngModelChange)="setModifier(index,$event)"><option value="">Add modifier…</option>@for (item of modifierOptions; track item.code) { <option [value]="item.code">–{{ item.code }} · {{ item.description }}</option> }</select></label>
928
+ <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
929
+ <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
930
+ <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
931
+ </div> }
932
+ <datalist id="mb-procedures">@for (item of procedureOptions; track item.code) { <option [value]="item.code">{{ item.description }}</option> }</datalist>
933
+ <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
934
+ </fieldset>
935
+
936
+ <fieldset><legend>Attachments</legend>
937
+ @for (attachment of workingAttachments; track attachment.filename; let index = $index) { <div class="attachment" [class.locked]="attachment.locked"><div><strong>{{ attachment.filename }}</strong><span>{{ attachment.description || attachment.badge || 'Supporting document' }}</span></div><div class="attachment-actions"><button type="button" (click)="previewAttachment(attachment)">Preview</button>@if (!attachment.locked) { <button type="button" (click)="removeAttachment(index)" aria-label="Remove attachment">×</button> }</div></div> }
938
+ <label class="dropzone"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>PDF supporting documents are sent with this immutable submission.</span></label>
939
+ </fieldset>
940
+
941
+ @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
942
+ <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>
943
+ </form>
944
+ `, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.line-head,.service-line{display:grid;grid-template-columns:1.7fr 1.5fr .55fr .75fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label{gap:4px}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
945
+ }], propDecorators: { initialBill: [{
946
+ type: Input
947
+ }], attachments: [{
948
+ type: Input
949
+ }], appearance: [{
950
+ type: Input
951
+ }], sessionEndpoint: [{
952
+ type: Input
953
+ }], getSession: [{
954
+ type: Input
955
+ }], apiBaseUrl: [{
956
+ type: Input
957
+ }], heading: [{
958
+ type: Input
959
+ }], description: [{
960
+ type: Input
961
+ }], submitLabel: [{
962
+ type: Input
963
+ }], procedureOptions: [{
964
+ type: Input
965
+ }], modifierOptions: [{
966
+ type: Input
967
+ }], taxonomyOptions: [{
968
+ type: Input
969
+ }], submitter: [{
970
+ type: Input
971
+ }], submitted: [{
972
+ type: Output
973
+ }], submissionError: [{
974
+ type: Output
975
+ }], billChange: [{
976
+ type: Output
977
+ }] } });
978
+
979
+ const THEME = {
980
+ mindbill: { a: "#238dbd", ac: "#fff", b: "#dbe6ea", r: "8px", font: "Inter,system-ui,sans-serif" },
981
+ "qme-companion": { a: "#53b5dc", ac: "#173542", b: "#d7e5eb", r: "8px", font: "Inter,system-ui,sans-serif" },
982
+ "orange-bright": { a: "#f4510b", ac: "#fff", b: "#e7e1da", r: "10px", font: "Inter,system-ui,sans-serif" },
983
+ "clinical-blue": { a: "#1677ff", ac: "#fff", b: "#d9e2ec", r: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
984
+ };
985
+ class MindBillBillingManagementButtonComponent {
986
+ sessionEndpoint = "/api/mindbill/management-session";
987
+ sessionProvider;
988
+ label = "Billing management";
989
+ loadingLabel = "Opening billing…";
990
+ appearance = { preset: "mindbill" };
991
+ opened = new EventEmitter();
992
+ failed = new EventEmitter();
993
+ loading = false;
994
+ errorMessage = "";
995
+ get themeStyle() {
996
+ const base = THEME[this.appearance.preset ?? "mindbill"] ?? THEME["mindbill"];
997
+ return {
998
+ "--a": this.appearance.accentColor ?? base["a"],
999
+ "--ac": this.appearance.accentTextColor ?? base["ac"],
1000
+ "--b": this.appearance.borderColor ?? base["b"],
1001
+ "--r": this.appearance.controlRadius ?? base["r"],
1002
+ "--font": this.appearance.fontFamily ?? base["font"],
1003
+ };
1004
+ }
1005
+ async openBillingManagement() {
1006
+ if (this.loading || typeof window === "undefined")
1007
+ return;
1008
+ this.loading = true;
1009
+ this.errorMessage = "";
1010
+ const popup = window.open("about:blank", "_blank", "noopener,noreferrer");
1011
+ try {
1012
+ const result = this.sessionProvider
1013
+ ? await this.sessionProvider()
1014
+ : await this.fetchSession();
1015
+ const url = typeof result === "string" ? result : result.url;
1016
+ if (!url)
1017
+ throw new Error("The billing management session did not include a URL.");
1018
+ if (popup)
1019
+ popup.location.replace(url);
1020
+ else
1021
+ window.location.assign(url);
1022
+ this.opened.emit(url);
1023
+ }
1024
+ catch (error) {
1025
+ popup?.close();
1026
+ this.errorMessage = error instanceof Error ? error.message : "Billing management could not be opened.";
1027
+ this.failed.emit(error);
1028
+ }
1029
+ finally {
1030
+ this.loading = false;
1031
+ }
1032
+ }
1033
+ async fetchSession() {
1034
+ const response = await fetch(this.sessionEndpoint, {
1035
+ method: "POST",
1036
+ credentials: "same-origin",
1037
+ headers: { accept: "application/json" },
1038
+ });
1039
+ const body = await response.json().catch(() => ({}));
1040
+ if (!response.ok)
1041
+ throw new Error(body.message || "Billing management could not be opened.");
1042
+ const url = body.url ?? body.data?.url;
1043
+ if (!url)
1044
+ throw new Error("The billing management session did not include a URL.");
1045
+ return { url };
1046
+ }
1047
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingManagementButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1048
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingManagementButtonComponent, isStandalone: true, selector: "mindbill-billing-management-button", inputs: { sessionEndpoint: "sessionEndpoint", sessionProvider: "sessionProvider", label: "label", loadingLabel: "loadingLabel", appearance: "appearance" }, outputs: { opened: "opened", failed: "failed" }, ngImport: i0, template: `
1049
+ <div class="management" [ngStyle]="themeStyle">
1050
+ <button type="button" [disabled]="loading" (click)="openBillingManagement()">
1051
+ <span>{{ loading ? loadingLabel : label }}</span><span aria-hidden="true">↗</span>
1052
+ </button>
1053
+ @if (errorMessage) { <p role="alert">{{ errorMessage }}</p> }
1054
+ </div>
1055
+ `, isInline: true, styles: [":host{display:inline-block}.management{font-family:var(--font)}button{display:inline-flex;align-items:center;justify-content:center;gap:10px;border:1px solid var(--a);border-radius:var(--r);background:var(--a);color:var(--ac);padding:11px 16px;font:inherit;font-weight:750;cursor:pointer}button:disabled{opacity:.65;cursor:wait}p{max-width:360px;margin:7px 0 0;color:#b42318;font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
1056
+ }
1057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingManagementButtonComponent, decorators: [{
1058
+ type: Component,
1059
+ args: [{ selector: "mindbill-billing-management-button", standalone: true, imports: [CommonModule], template: `
1060
+ <div class="management" [ngStyle]="themeStyle">
1061
+ <button type="button" [disabled]="loading" (click)="openBillingManagement()">
1062
+ <span>{{ loading ? loadingLabel : label }}</span><span aria-hidden="true">↗</span>
1063
+ </button>
1064
+ @if (errorMessage) { <p role="alert">{{ errorMessage }}</p> }
1065
+ </div>
1066
+ `, styles: [":host{display:inline-block}.management{font-family:var(--font)}button{display:inline-flex;align-items:center;justify-content:center;gap:10px;border:1px solid var(--a);border-radius:var(--r);background:var(--a);color:var(--ac);padding:11px 16px;font:inherit;font-weight:750;cursor:pointer}button:disabled{opacity:.65;cursor:wait}p{max-width:360px;margin:7px 0 0;color:#b42318;font-size:13px}\n"] }]
1067
+ }], propDecorators: { sessionEndpoint: [{
1068
+ type: Input
1069
+ }], sessionProvider: [{
1070
+ type: Input
1071
+ }], label: [{
1072
+ type: Input
1073
+ }], loadingLabel: [{
1074
+ type: Input
1075
+ }], appearance: [{
1076
+ type: Input
1077
+ }], opened: [{
1078
+ type: Output
1079
+ }], failed: [{
1080
+ type: Output
1081
+ }] } });
1082
+
440
1083
  /**
441
1084
  * Generated bundle index. Do not edit.
442
1085
  */
443
1086
 
444
- export { MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillingDashboardComponent, MindBillBillingReportComponent, MindBillLifecycleStore, buildMindBillReportCsv, buildMindBillReportRows, ensureTrailingProcedureLine, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, summarizeMindBillDashboard };
1087
+ export { MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillSubmissionComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillLifecycleStore, buildMindBillReportCsv, buildMindBillReportRows, ensureTrailingProcedureLine, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, summarizeMindBillDashboard };
445
1088
  //# sourceMappingURL=mindbill-angular.mjs.map