@mindbill/angular 0.4.2 → 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
- import { signal, computed, EventEmitter, effect, Output, Input, Component } from '@angular/core';
3
- import { createBillLifecycleClient } from '@mindbill/browser';
2
+ import { signal, computed, EventEmitter, effect, Output, Input, Component, Directive } from '@angular/core';
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,9 +275,814 @@ function ensureTrailingProcedureLine(lines) {
275
275
  : entered;
276
276
  }
277
277
 
278
+ const THEME$2 = {
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
+ "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
+ "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" },
282
+ "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" },
283
+ };
284
+ function mindBillAgingDays(bill, now = new Date()) {
285
+ if (bill.agingDays != null)
286
+ return Math.max(0, Math.floor(bill.agingDays));
287
+ const source = bill.submittedAt ?? bill.updatedAt;
288
+ if (!source)
289
+ return 0;
290
+ const time = new Date(source).getTime();
291
+ return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
292
+ }
293
+ function mindBillAgingBucket(days) {
294
+ if (days <= 30)
295
+ return "current";
296
+ if (days <= 60)
297
+ return "31-60";
298
+ if (days <= 90)
299
+ return "61-90";
300
+ return "91+";
301
+ }
302
+ function summarizeMindBillDashboard(bills, now = new Date()) {
303
+ const aging = [
304
+ { id: "current", label: "0–30 days", count: 0, balance: 0 },
305
+ { id: "31-60", label: "31–60 days", count: 0, balance: 0 },
306
+ { id: "61-90", label: "61–90 days", count: 0, balance: 0 },
307
+ { id: "91+", label: "91+ days", count: 0, balance: 0 },
308
+ ];
309
+ for (const bill of bills) {
310
+ const bucket = aging.find((item) => item.id === mindBillAgingBucket(mindBillAgingDays(bill, now)));
311
+ bucket.count += 1;
312
+ bucket.balance += bill.balanceDue;
313
+ }
314
+ return {
315
+ totalBilled: bills.reduce((sum, item) => sum + item.totalCharge, 0),
316
+ totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
317
+ outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
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,
321
+ bills,
322
+ aging,
323
+ };
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
+ }
331
+ function buildMindBillReportRows(bills, dimension) {
332
+ const rows = new Map();
333
+ for (const bill of bills) {
334
+ const key = dimension === "payer" ? bill.payerName || "Unassigned payer" : dimension === "aging" ? mindBillAgingBucket(mindBillAgingDays(bill)) : bill.state;
335
+ const label = key.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
336
+ const row = rows.get(key) ?? { key, label, billCount: 0, totalBilled: 0, totalPaid: 0, balanceDue: 0 };
337
+ row.billCount += 1;
338
+ row.totalBilled += bill.totalCharge;
339
+ row.totalPaid += bill.totalPaid;
340
+ row.balanceDue += bill.balanceDue;
341
+ rows.set(key, row);
342
+ }
343
+ return [...rows.values()].sort((a, b) => b.balanceDue - a.balanceDue || a.label.localeCompare(b.label));
344
+ }
345
+ function buildMindBillReportCsv(rows) {
346
+ const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
347
+ return ["Group,Bills,Total billed,Total paid,Balance due", ...rows.map((row) => [row.label, row.billCount, row.totalBilled, row.totalPaid, row.balanceDue].map(quote).join(","))].join("\n");
348
+ }
349
+ class DashboardBase {
350
+ appearance = { preset: "mindbill" };
351
+ get themeStyle() {
352
+ const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
353
+ return {
354
+ "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
355
+ "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
356
+ "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
357
+ "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
358
+ "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
359
+ };
360
+ }
361
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
362
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: DashboardBase, isStandalone: true, inputs: { appearance: "appearance" }, ngImport: i0 });
363
+ }
364
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, decorators: [{
365
+ type: Directive
366
+ }], propDecorators: { appearance: [{
367
+ type: Input
368
+ }] } });
369
+ const sharedStyles = `
370
+ :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)}
371
+ .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}
372
+ `;
373
+ class MindBillBillAgingSummaryComponent extends DashboardBase {
374
+ buckets = [];
375
+ selectedBucket = "";
376
+ bucketSelected = new EventEmitter();
377
+ width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
378
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
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" }] });
380
+ }
381
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
382
+ type: Component,
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"] }]
384
+ }], propDecorators: { buckets: [{
385
+ type: Input
386
+ }], selectedBucket: [{
387
+ type: Input
388
+ }], bucketSelected: [{
389
+ type: Output
390
+ }] } });
391
+ class MindBillBillListComponent extends DashboardBase {
392
+ bills = [];
393
+ billSelected = new EventEmitter();
394
+ age(bill) { return mindBillAgingDays(bill); }
395
+ label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
396
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
397
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillListComponent, isStandalone: true, selector: "mindbill-bill-list", inputs: { bills: "bills" }, outputs: { billSelected: "billSelected" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</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", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
398
+ }
399
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, decorators: [{
400
+ type: Component,
401
+ args: [{ selector: "mindbill-bill-list", standalone: true, imports: [CommonModule], template: `<section class="mbd list card" [ngStyle]="themeStyle"><div class="head"><strong>Bill</strong><strong>Payer</strong><strong>Status</strong><strong>Age</strong><strong>Balance</strong></div>@for (bill of bills; track bill.id) { <button type="button" (click)="billSelected.emit(bill)"><span><b>{{ bill.patientName }}</b><small>{{ bill.claimNumber || bill.workItemLabel || bill.externalId || bill.id }}</small></span><span>{{ bill.payerName || '—' }}</span><span><i class="pill">{{ label(bill.state) }}</i></span><span>{{ age(bill) }} days</span><span class="money"><b>{{ bill.balanceDue | currency }}</b><small>{{ bill.totalPaid | currency }} paid</small></span></button> } @empty { <p class="empty">No bills match these filters.</p> }</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", ".list{overflow:hidden}.head,.list button{display:grid;grid-template-columns:1.6fr 1.2fr .85fr .55fr .8fr;gap:18px;align-items:center;text-align:left}.head{padding:11px 16px;background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.list button{width:100%;border:0;border-top:1px solid var(--b);background:var(--s);color:var(--t);padding:15px 16px;cursor:pointer}.list button:hover{background:color-mix(in srgb,var(--a) 5%,var(--s))}.list span,.list small{min-width:0}.list small{display:block;color:var(--m);margin-top:4px}.money{text-align:right}.empty{padding:24px;text-align:center;color:var(--m)}@media(max-width:760px){.head{display:none}.list button{grid-template-columns:1fr auto;gap:10px}.list button>span:nth-child(2){grid-column:1}.list button>span:nth-child(3){grid-row:1;grid-column:2}.list button>span:nth-child(4){grid-column:1}.money{grid-column:2;grid-row:2/4}}\n"] }]
402
+ }], propDecorators: { bills: [{
403
+ type: Input
404
+ }], billSelected: [{
405
+ type: Output
406
+ }] } });
407
+ class MindBillBillingDashboardComponent extends DashboardBase {
408
+ bills = [];
409
+ heading = "Billing dashboard";
410
+ description = "Track every submitted bill from payer acceptance through payment.";
411
+ billSelected = new EventEmitter();
412
+ createBill = new EventEmitter();
413
+ query = "";
414
+ state = "";
415
+ agingBucket = "";
416
+ get summary() { return summarizeMindBillDashboard(this.bills); }
417
+ get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
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; }
421
+ label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
422
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
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" }] });
424
+ }
425
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
426
+ type: Component,
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"] }]
428
+ }], propDecorators: { bills: [{
429
+ type: Input
430
+ }], heading: [{
431
+ type: Input
432
+ }], description: [{
433
+ type: Input
434
+ }], billSelected: [{
435
+ type: Output
436
+ }], createBill: [{
437
+ type: Output
438
+ }] } });
439
+ class MindBillBillingReportComponent extends DashboardBase {
440
+ bills = [];
441
+ heading = "Billing report";
442
+ dimension = "status";
443
+ get rows() { return buildMindBillReportRows(this.bills, this.dimension); }
444
+ downloadCsv() { const blob = new Blob([buildMindBillReportCsv(this.rows)], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `mindbill-${this.dimension}-report.csv`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
445
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
446
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillingReportComponent, isStandalone: true, selector: "mindbill-billing-report", inputs: { bills: "bills", heading: "heading" }, usesInheritance: true, ngImport: i0, template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</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", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\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.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: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
447
+ }
448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, decorators: [{
449
+ type: Component,
450
+ args: [{ selector: "mindbill-billing-report", standalone: true, imports: [CommonModule, FormsModule], template: `<section class="mbd report card" [ngStyle]="themeStyle"><header><div><h2>{{ heading }}</h2><p>Operational totals grouped for reconciliation and follow-up.</p></div><div><select [(ngModel)]="dimension"><option value="status">Status</option><option value="payer">Payer</option><option value="aging">Aging</option></select><button type="button" (click)="downloadCsv()">Export CSV</button></div></header><div class="thead"><b>Group</b><b>Bills</b><b>Total billed</b><b>Total paid</b><b>Balance due</b></div>@for (row of rows; track row.key) { <div class="row"><strong>{{ row.label }}</strong><span>{{ row.billCount }}</span><span>{{ row.totalBilled | currency }}</span><span>{{ row.totalPaid | currency }}</span><b>{{ row.balanceDue | currency }}</b></div> }</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", ".report{overflow:hidden}.report header{display:flex;justify-content:space-between;align-items:center;padding:18px}.report h2{margin:0;font-size:22px}.report p{margin:4px 0 0;color:var(--m)}.report header div:last-child{display:flex;gap:8px}.report select,.report button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);color:var(--t);padding:9px 12px}.report button{background:var(--a);border-color:var(--a);color:var(--ac);font-weight:750;cursor:pointer}.thead,.row{display:grid;grid-template-columns:1.5fr .5fr 1fr 1fr 1fr;gap:14px;padding:12px 18px;border-top:1px solid var(--b);align-items:center}.thead{background:color-mix(in srgb,var(--bg) 78%,var(--s));color:var(--m);font-size:11px;text-transform:uppercase}.row span,.row b:not(:first-child){text-align:right;font-variant-numeric:tabular-nums}@media(max-width:700px){.report header{align-items:flex-start}.thead{display:none}.row{grid-template-columns:1fr 1fr}.row>*:nth-child(n+2){text-align:right}.row strong{grid-column:1}.row span:nth-child(2){grid-column:2}.row span:nth-child(3):before{content:\"Billed \";color:var(--m)}.row span:nth-child(4):before{content:\"Paid \";color:var(--m)}.row b:before{content:\"Due \";color:var(--m)}}\n"] }]
451
+ }], propDecorators: { bills: [{
452
+ type: Input
453
+ }], heading: [{
454
+ type: Input
455
+ }] } });
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
+
278
1083
  /**
279
1084
  * Generated bundle index. Do not edit.
280
1085
  */
281
1086
 
282
- export { MindBillBillLifecycleComponent, MindBillLifecycleStore, ensureTrailingProcedureLine, isEmptyProcedureLine };
1087
+ export { MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillSubmissionComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillLifecycleStore, buildMindBillReportCsv, buildMindBillReportRows, ensureTrailingProcedureLine, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, summarizeMindBillDashboard };
283
1088
  //# sourceMappingURL=mindbill-angular.mjs.map