@mindbill/angular 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/fesm2022/mindbill-angular.mjs +1816 -1669
- package/fesm2022/mindbill-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/mindbill-angular.d.ts +184 -172
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Input, Component, EventEmitter,
|
|
3
|
-
import { createBillLifecycleClient,
|
|
2
|
+
import { signal, computed, Input, Component, EventEmitter, Output, inject, ChangeDetectorRef, HostListener, effect, Directive } from '@angular/core';
|
|
3
|
+
import { createBillLifecycleClient, createBillReferenceClient, createBillSubmissionClient, BILL_TASKS_AGING_BUCKETS, createOrganizationClient } 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';
|
|
@@ -54,6 +54,7 @@ class MindBillLifecycleStore {
|
|
|
54
54
|
getEor(id) { return this.requireClient().getEor(id); }
|
|
55
55
|
closeBill(input) { return this.mutate(() => this.requireClient().closeBill(input)); }
|
|
56
56
|
postPayment(input) { return this.mutate(() => this.requireClient().postPayment(input)); }
|
|
57
|
+
resubmitBill(input) { return this.mutate(() => this.requireClient().resubmitBill(input)); }
|
|
57
58
|
submitSecondReview(input) { return this.mutate(() => this.requireClient().submitSecondReview(input)); }
|
|
58
59
|
requireClient() {
|
|
59
60
|
if (!this.client)
|
|
@@ -188,363 +189,430 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
188
189
|
type: Input
|
|
189
190
|
}] } });
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
192
|
+
/** Date helpers shared by the Angular submission surfaces. Mirrors the React
|
|
193
|
+
* package's parseBillSubmissionDate/formatBillSubmissionDate exactly so both
|
|
194
|
+
* frameworks accept and render the same values. */
|
|
195
|
+
function parseMindBillSubmissionDate(value) {
|
|
196
|
+
const trimmed = value.trim();
|
|
197
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
198
|
+
const us = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/.exec(trimmed);
|
|
199
|
+
const digits = /^(\d{2})(\d{2})(\d{4})$/.exec(trimmed.replace(/\D/g, ""));
|
|
200
|
+
const match = iso ? [iso[2], iso[3], iso[1]] : us ? [us[1], us[2], us[3]] : digits ? [digits[1], digits[2], digits[3]] : null;
|
|
201
|
+
if (!match)
|
|
202
|
+
return undefined;
|
|
203
|
+
const month = Number(match[0]);
|
|
204
|
+
const day = Number(match[1]);
|
|
205
|
+
const year = Number(match[2]);
|
|
206
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
207
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
|
|
208
|
+
return undefined;
|
|
209
|
+
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
210
|
+
}
|
|
211
|
+
function formatMindBillSubmissionDate(value) {
|
|
212
|
+
const parsed = value ? parseMindBillSubmissionDate(value) : undefined;
|
|
213
|
+
if (!parsed)
|
|
214
|
+
return "";
|
|
215
|
+
const [year, month, day] = parsed.split("-");
|
|
216
|
+
return `${month}/${day}/${year}`;
|
|
217
|
+
}
|
|
218
|
+
/** Accepts complete CPT (5 digits), HCPCS (letter + 4 digits), or California
|
|
219
|
+
* medical-legal codes typed directly into the procedure picker. */
|
|
220
|
+
function mindBillCustomProcedureOption(query) {
|
|
221
|
+
const code = query.trim().toUpperCase().replace(/\s+/g, "");
|
|
222
|
+
if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code))
|
|
223
|
+
return null;
|
|
224
|
+
return { id: code, label: code, detail: "Use this CPT, HCPCS, or medical-legal code" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Styled searchable dropdown shared by the submission form's procedure,
|
|
228
|
+
* modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
|
|
229
|
+
* package's ComboBox: query filtering, optional custom-code creation,
|
|
230
|
+
* loading states, and infinite scroll via (endReached). */
|
|
231
|
+
class MindBillComboBoxComponent {
|
|
232
|
+
value = "";
|
|
233
|
+
placeholder = "";
|
|
234
|
+
options = [];
|
|
235
|
+
disabled = false;
|
|
236
|
+
loading = false;
|
|
237
|
+
loadingMore = false;
|
|
238
|
+
invalid = false;
|
|
239
|
+
preserveValueOnOpen = false;
|
|
240
|
+
ariaLabel = "";
|
|
241
|
+
createOption;
|
|
242
|
+
opened = new EventEmitter();
|
|
243
|
+
queryChange = new EventEmitter();
|
|
244
|
+
endReached = new EventEmitter();
|
|
245
|
+
selected = new EventEmitter();
|
|
246
|
+
open = false;
|
|
247
|
+
query = "";
|
|
248
|
+
closeTimer;
|
|
249
|
+
get visible() {
|
|
250
|
+
const q = this.query.trim().toLowerCase();
|
|
251
|
+
const matches = q
|
|
252
|
+
? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
|
|
253
|
+
: this.options;
|
|
254
|
+
const custom = this.createOption?.(this.query) ?? null;
|
|
255
|
+
return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
|
|
256
|
+
? [...matches, custom]
|
|
257
|
+
: matches;
|
|
258
|
+
}
|
|
259
|
+
get showMenu() {
|
|
260
|
+
return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
|
|
261
|
+
}
|
|
262
|
+
onFocus() {
|
|
263
|
+
clearTimeout(this.closeTimer);
|
|
264
|
+
const next = this.preserveValueOnOpen ? this.value : "";
|
|
265
|
+
this.open = true;
|
|
266
|
+
this.query = next;
|
|
267
|
+
this.opened.emit();
|
|
268
|
+
if (next)
|
|
269
|
+
this.queryChange.emit(next);
|
|
270
|
+
}
|
|
271
|
+
onInput(event) {
|
|
272
|
+
this.open = true;
|
|
273
|
+
this.query = event.target.value;
|
|
274
|
+
this.queryChange.emit(this.query);
|
|
275
|
+
}
|
|
276
|
+
onBlur() {
|
|
277
|
+
this.closeTimer = setTimeout(() => { this.open = false; }, 120);
|
|
278
|
+
}
|
|
279
|
+
onScroll(event) {
|
|
280
|
+
const menu = event.currentTarget;
|
|
281
|
+
if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120)
|
|
282
|
+
this.endReached.emit();
|
|
283
|
+
}
|
|
284
|
+
choose(option) {
|
|
285
|
+
this.selected.emit(option);
|
|
286
|
+
this.open = false;
|
|
287
|
+
this.query = "";
|
|
288
|
+
}
|
|
289
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
290
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillComboBoxComponent, isStandalone: true, selector: "mindbill-combo-box", inputs: { value: "value", placeholder: "placeholder", options: "options", disabled: "disabled", loading: "loading", loadingMore: "loadingMore", invalid: "invalid", preserveValueOnOpen: "preserveValueOnOpen", ariaLabel: "ariaLabel", createOption: "createOption" }, outputs: { opened: "opened", queryChange: "queryChange", endReached: "endReached", selected: "selected" }, ngImport: i0, template: `
|
|
291
|
+
<div class="combo">
|
|
292
|
+
<input
|
|
293
|
+
type="text"
|
|
294
|
+
role="combobox"
|
|
295
|
+
autocomplete="off"
|
|
296
|
+
[attr.aria-label]="ariaLabel"
|
|
297
|
+
[attr.aria-invalid]="invalid || null"
|
|
298
|
+
[attr.aria-expanded]="showMenu"
|
|
299
|
+
[disabled]="disabled"
|
|
300
|
+
[placeholder]="placeholder"
|
|
301
|
+
[value]="open ? query : value"
|
|
302
|
+
(focus)="onFocus()"
|
|
303
|
+
(click)="onFocus()"
|
|
304
|
+
(input)="onInput($event)"
|
|
305
|
+
(blur)="onBlur()"
|
|
306
|
+
/>
|
|
307
|
+
@if (showMenu) {
|
|
308
|
+
<div class="menu" role="listbox" (scroll)="onScroll($event)">
|
|
309
|
+
@if (loading && !visible.length) { <div class="status">Loading…</div> }
|
|
310
|
+
@else if (visible.length) {
|
|
311
|
+
@for (option of visible; track option.id) {
|
|
312
|
+
<button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
|
|
313
|
+
<strong>{{ option.label }}</strong>
|
|
314
|
+
@if (option.detail) { <small>{{ option.detail }}</small> }
|
|
315
|
+
</button>
|
|
217
316
|
}
|
|
218
|
-
|
|
317
|
+
} @else { <div class="status">No matches</div> }
|
|
318
|
+
@if (loadingMore) { <div class="status">Loading more…</div> }
|
|
319
|
+
</div>
|
|
320
|
+
}
|
|
321
|
+
</div>
|
|
322
|
+
`, isInline: true, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
323
|
+
}
|
|
324
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, decorators: [{
|
|
325
|
+
type: Component,
|
|
326
|
+
args: [{ selector: "mindbill-combo-box", standalone: true, imports: [CommonModule], template: `
|
|
327
|
+
<div class="combo">
|
|
328
|
+
<input
|
|
329
|
+
type="text"
|
|
330
|
+
role="combobox"
|
|
331
|
+
autocomplete="off"
|
|
332
|
+
[attr.aria-label]="ariaLabel"
|
|
333
|
+
[attr.aria-invalid]="invalid || null"
|
|
334
|
+
[attr.aria-expanded]="showMenu"
|
|
335
|
+
[disabled]="disabled"
|
|
336
|
+
[placeholder]="placeholder"
|
|
337
|
+
[value]="open ? query : value"
|
|
338
|
+
(focus)="onFocus()"
|
|
339
|
+
(click)="onFocus()"
|
|
340
|
+
(input)="onInput($event)"
|
|
341
|
+
(blur)="onBlur()"
|
|
342
|
+
/>
|
|
343
|
+
@if (showMenu) {
|
|
344
|
+
<div class="menu" role="listbox" (scroll)="onScroll($event)">
|
|
345
|
+
@if (loading && !visible.length) { <div class="status">Loading…</div> }
|
|
346
|
+
@else if (visible.length) {
|
|
347
|
+
@for (option of visible; track option.id) {
|
|
348
|
+
<button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
|
|
349
|
+
<strong>{{ option.label }}</strong>
|
|
350
|
+
@if (option.detail) { <small>{{ option.detail }}</small> }
|
|
351
|
+
</button>
|
|
352
|
+
}
|
|
353
|
+
} @else { <div class="status">No matches</div> }
|
|
354
|
+
@if (loadingMore) { <div class="status">Loading more…</div> }
|
|
355
|
+
</div>
|
|
356
|
+
}
|
|
357
|
+
</div>
|
|
358
|
+
`, styles: [":host{display:block}.combo{position:relative}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}.menu{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:30;max-height:320px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.menu button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid color-mix(in srgb,var(--b) 60%,transparent);background:var(--s);padding:11px 14px;text-align:left;color:var(--t);font:inherit;cursor:pointer}.menu button:last-child{border-bottom:0}.menu button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.menu strong{font-weight:720}.menu small{color:var(--m);font-size:12px}.status{padding:11px 14px;color:var(--m);font-size:13px}\n"] }]
|
|
359
|
+
}], propDecorators: { value: [{
|
|
360
|
+
type: Input
|
|
361
|
+
}], placeholder: [{
|
|
362
|
+
type: Input
|
|
363
|
+
}], options: [{
|
|
364
|
+
type: Input
|
|
365
|
+
}], disabled: [{
|
|
366
|
+
type: Input
|
|
367
|
+
}], loading: [{
|
|
368
|
+
type: Input
|
|
369
|
+
}], loadingMore: [{
|
|
370
|
+
type: Input
|
|
371
|
+
}], invalid: [{
|
|
372
|
+
type: Input
|
|
373
|
+
}], preserveValueOnOpen: [{
|
|
374
|
+
type: Input
|
|
375
|
+
}], ariaLabel: [{
|
|
376
|
+
type: Input
|
|
377
|
+
}], createOption: [{
|
|
378
|
+
type: Input
|
|
379
|
+
}], opened: [{
|
|
380
|
+
type: Output
|
|
381
|
+
}], queryChange: [{
|
|
382
|
+
type: Output
|
|
383
|
+
}], endReached: [{
|
|
384
|
+
type: Output
|
|
385
|
+
}], selected: [{
|
|
386
|
+
type: Output
|
|
387
|
+
}] } });
|
|
388
|
+
/** MM/DD/YYYY masked date input. Emits only parseable ISO dates (or "" when
|
|
389
|
+
* cleared) and reformats the display once a complete date round-trips, exactly
|
|
390
|
+
* like the React package's TextDateInput. */
|
|
391
|
+
class MindBillDateInputComponent {
|
|
392
|
+
value;
|
|
393
|
+
disabled = false;
|
|
394
|
+
ariaLabel = "";
|
|
395
|
+
valueChange = new EventEmitter();
|
|
396
|
+
display = "";
|
|
397
|
+
invalid = false;
|
|
398
|
+
ngOnChanges(changes) {
|
|
399
|
+
if (changes["value"]) {
|
|
400
|
+
this.display = formatMindBillSubmissionDate(this.value);
|
|
401
|
+
this.invalid = false;
|
|
219
402
|
}
|
|
220
403
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
404
|
+
onInput(event) {
|
|
405
|
+
const next = event.target.value;
|
|
406
|
+
const parsed = parseMindBillSubmissionDate(next);
|
|
407
|
+
this.display = next;
|
|
408
|
+
this.invalid = Boolean(next && !parsed);
|
|
409
|
+
if (!next.trim())
|
|
410
|
+
this.valueChange.emit("");
|
|
411
|
+
else if (parsed)
|
|
412
|
+
this.valueChange.emit(parsed);
|
|
224
413
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this.panel = "review";
|
|
245
|
-
else if (action === "close")
|
|
246
|
-
this.panel = "close";
|
|
247
|
-
else if (action === "view_eor" && data.eors[0])
|
|
248
|
-
void this.openEor(data.eors[0].id, data.eors[0].filename); }
|
|
249
|
-
async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
|
|
250
|
-
async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
|
|
251
|
-
async postPayment() { await this.store.postPayment(this.payment); this.panel = ""; this.notice = "Payment posted."; }
|
|
252
|
-
async submitReview() { await this.store.submitSecondReview({ ...this.review, disputedAmount: this.review.disputedAmount > 0 ? this.review.disputedAmount : undefined, attachmentIds: this.store.data()?.bill.attachments.map((document) => document.id) ?? [], route: "ebill" }); this.panel = ""; this.notice = "Second Review submitted."; }
|
|
253
|
-
async closeBill() { if (!this.closeReason.trim())
|
|
254
|
-
return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
|
|
255
|
-
openBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noopener"; link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); }
|
|
256
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
257
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillLifecycleComponent, isStandalone: true, selector: "mindbill-bill-lifecycle", inputs: { billId: "billId", sessionEndpoint: "sessionEndpoint", apiBaseUrl: "apiBaseUrl", getSession: "getSession", refreshInterval: "refreshInterval", appearance: "appearance" }, outputs: { billingError: "billingError" }, usesOnChanges: true, ngImport: i0, template: `
|
|
258
|
-
<section class="mb" [ngStyle]="themeStyle">
|
|
259
|
-
@if (!billId) {
|
|
260
|
-
<div class="state error"><strong>A submitted bill ID is required.</strong></div>
|
|
261
|
-
} @else if (store.loading() && !store.data()) {
|
|
262
|
-
<div class="state">Loading billing…</div>
|
|
263
|
-
} @else if (store.error() && !store.data()) {
|
|
264
|
-
<div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
|
|
265
|
-
} @else if (store.data(); as data) {
|
|
266
|
-
<header class="summary">
|
|
267
|
-
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
268
|
-
<div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
|
|
269
|
-
</header>
|
|
270
|
-
|
|
271
|
-
@if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
|
|
272
|
-
<mindbill-bill-rejection-notice
|
|
273
|
-
class="rejection"
|
|
274
|
-
[rejection]="data.rejection"
|
|
275
|
-
[submittedAt]="data.lifecycle.submittedAt ?? null"
|
|
276
|
-
[appearance]="appearance"
|
|
277
|
-
/>
|
|
278
|
-
} @else {
|
|
279
|
-
<section class="card progress" aria-label="Bill lifecycle">
|
|
280
|
-
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
281
|
-
</section>
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
<section class="card snapshot" aria-label="Bill snapshot">
|
|
285
|
-
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
286
|
-
<dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
|
|
287
|
-
</section>
|
|
288
|
-
|
|
289
|
-
<div class="columns">
|
|
290
|
-
<section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty & interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
|
|
291
|
-
<section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
|
-
<section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
|
|
295
|
-
|
|
296
|
-
<section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
|
|
297
|
-
|
|
298
|
-
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
|
|
299
|
-
@if (data.history?.length) {
|
|
300
|
-
<!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
|
|
301
|
-
expandable rows (documents + compliance due dates), notes pale-yellow,
|
|
302
|
-
277 responses expand into decoded status-code sentences. Rows arrive
|
|
303
|
-
pre-worded from the MindBill API (BillLifecycleData.history). -->
|
|
304
|
-
<div class="history">
|
|
305
|
-
<div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
|
|
306
|
-
<ul class="history-rows">
|
|
307
|
-
@for (entry of data.history; track entry.id) {
|
|
308
|
-
<li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
|
|
309
|
-
<button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
|
|
310
|
-
<span class="h-date">{{ historyDate(entry.date) }}</span>
|
|
311
|
-
<span class="h-action">{{ entry.action }}</span>
|
|
312
|
-
<span class="h-actor">{{ entry.actor || '—' }}</span>
|
|
313
|
-
<span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
|
|
314
|
-
</button>
|
|
315
|
-
@if (entry.details; as details) {
|
|
316
|
-
@if (isHistoryOpen(entry.id)) {
|
|
317
|
-
<div class="history-details">
|
|
318
|
-
@if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
|
|
319
|
-
@if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
|
|
320
|
-
@if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
|
|
321
|
-
@if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
|
|
322
|
-
@if (details.text) { <p class="h-text">{{ details.text }}</p> }
|
|
323
|
-
</div>
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
</li>
|
|
327
|
-
}
|
|
328
|
-
</ul>
|
|
329
|
-
</div>
|
|
330
|
-
} @else {
|
|
331
|
-
<ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
|
|
332
|
-
}
|
|
333
|
-
</section>
|
|
334
|
-
|
|
335
|
-
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
336
|
-
<div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
|
|
337
|
-
|
|
338
|
-
@if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
|
|
339
|
-
@if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
|
|
340
|
-
@if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
|
|
341
|
-
}
|
|
342
|
-
</section>
|
|
343
|
-
`, isInline: true, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{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.ɵ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.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MindBillBillRejectionNoticeComponent, selector: "mindbill-bill-rejection-notice", inputs: ["rejection", "submittedAt", "title", "appearance"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
|
|
414
|
+
onBlur() {
|
|
415
|
+
this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
|
|
416
|
+
}
|
|
417
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
418
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillDateInputComponent, isStandalone: true, selector: "mindbill-date-input", inputs: { value: "value", disabled: "disabled", ariaLabel: "ariaLabel" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
|
|
419
|
+
<input
|
|
420
|
+
type="text"
|
|
421
|
+
inputmode="numeric"
|
|
422
|
+
autocomplete="off"
|
|
423
|
+
placeholder="MM/DD/YYYY"
|
|
424
|
+
[attr.aria-label]="ariaLabel"
|
|
425
|
+
[attr.aria-invalid]="invalid || null"
|
|
426
|
+
[disabled]="disabled"
|
|
427
|
+
[value]="display"
|
|
428
|
+
(input)="onInput($event)"
|
|
429
|
+
(blur)="onBlur()"
|
|
430
|
+
/>
|
|
431
|
+
@if (invalid) { <small>Use MM/DD/YYYY</small> }
|
|
432
|
+
`, isInline: true, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
344
433
|
}
|
|
345
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
434
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
|
|
346
435
|
type: Component,
|
|
347
|
-
args: [{ selector: "mindbill-
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
class="rejection"
|
|
364
|
-
[rejection]="data.rejection"
|
|
365
|
-
[submittedAt]="data.lifecycle.submittedAt ?? null"
|
|
366
|
-
[appearance]="appearance"
|
|
367
|
-
/>
|
|
368
|
-
} @else {
|
|
369
|
-
<section class="card progress" aria-label="Bill lifecycle">
|
|
370
|
-
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
371
|
-
</section>
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
<section class="card snapshot" aria-label="Bill snapshot">
|
|
375
|
-
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
376
|
-
<dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
|
|
377
|
-
</section>
|
|
378
|
-
|
|
379
|
-
<div class="columns">
|
|
380
|
-
<section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty & interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
|
|
381
|
-
<section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
|
|
382
|
-
</div>
|
|
383
|
-
|
|
384
|
-
<section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
|
|
385
|
-
|
|
386
|
-
<section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
|
|
387
|
-
|
|
388
|
-
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
|
|
389
|
-
@if (data.history?.length) {
|
|
390
|
-
<!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
|
|
391
|
-
expandable rows (documents + compliance due dates), notes pale-yellow,
|
|
392
|
-
277 responses expand into decoded status-code sentences. Rows arrive
|
|
393
|
-
pre-worded from the MindBill API (BillLifecycleData.history). -->
|
|
394
|
-
<div class="history">
|
|
395
|
-
<div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
|
|
396
|
-
<ul class="history-rows">
|
|
397
|
-
@for (entry of data.history; track entry.id) {
|
|
398
|
-
<li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
|
|
399
|
-
<button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
|
|
400
|
-
<span class="h-date">{{ historyDate(entry.date) }}</span>
|
|
401
|
-
<span class="h-action">{{ entry.action }}</span>
|
|
402
|
-
<span class="h-actor">{{ entry.actor || '—' }}</span>
|
|
403
|
-
<span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
|
|
404
|
-
</button>
|
|
405
|
-
@if (entry.details; as details) {
|
|
406
|
-
@if (isHistoryOpen(entry.id)) {
|
|
407
|
-
<div class="history-details">
|
|
408
|
-
@if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
|
|
409
|
-
@if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
|
|
410
|
-
@if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
|
|
411
|
-
@if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
|
|
412
|
-
@if (details.text) { <p class="h-text">{{ details.text }}</p> }
|
|
413
|
-
</div>
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
</li>
|
|
417
|
-
}
|
|
418
|
-
</ul>
|
|
419
|
-
</div>
|
|
420
|
-
} @else {
|
|
421
|
-
<ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
|
|
422
|
-
}
|
|
423
|
-
</section>
|
|
424
|
-
|
|
425
|
-
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
426
|
-
<div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
|
|
427
|
-
|
|
428
|
-
@if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
|
|
429
|
-
@if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
|
|
430
|
-
@if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
|
|
431
|
-
}
|
|
432
|
-
</section>
|
|
433
|
-
`, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}\n"] }]
|
|
434
|
-
}], ctorParameters: () => [], propDecorators: { billId: [{
|
|
435
|
-
type: Input
|
|
436
|
-
}], sessionEndpoint: [{
|
|
437
|
-
type: Input
|
|
438
|
-
}], apiBaseUrl: [{
|
|
439
|
-
type: Input
|
|
440
|
-
}], getSession: [{
|
|
436
|
+
args: [{ selector: "mindbill-date-input", standalone: true, imports: [CommonModule], template: `
|
|
437
|
+
<input
|
|
438
|
+
type="text"
|
|
439
|
+
inputmode="numeric"
|
|
440
|
+
autocomplete="off"
|
|
441
|
+
placeholder="MM/DD/YYYY"
|
|
442
|
+
[attr.aria-label]="ariaLabel"
|
|
443
|
+
[attr.aria-invalid]="invalid || null"
|
|
444
|
+
[disabled]="disabled"
|
|
445
|
+
[value]="display"
|
|
446
|
+
(input)="onInput($event)"
|
|
447
|
+
(blur)="onBlur()"
|
|
448
|
+
/>
|
|
449
|
+
@if (invalid) { <small>Use MM/DD/YYYY</small> }
|
|
450
|
+
`, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"] }]
|
|
451
|
+
}], propDecorators: { value: [{
|
|
441
452
|
type: Input
|
|
442
|
-
}],
|
|
453
|
+
}], disabled: [{
|
|
443
454
|
type: Input
|
|
444
|
-
}],
|
|
455
|
+
}], ariaLabel: [{
|
|
445
456
|
type: Input
|
|
446
|
-
}],
|
|
457
|
+
}], valueChange: [{
|
|
447
458
|
type: Output
|
|
448
459
|
}] } });
|
|
449
460
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
461
|
+
const MAX_PDF_BYTES = 25 * 1024 * 1024;
|
|
462
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
463
|
+
const MAX_DOCUMENTS = 20;
|
|
464
|
+
const PROCEDURES = [
|
|
465
|
+
{ code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
|
|
466
|
+
{ code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
|
|
467
|
+
{ code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
|
|
468
|
+
{ code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
|
|
469
|
+
{ code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
|
|
470
|
+
{ code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
|
|
471
|
+
{ code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
|
|
472
|
+
{ code: "90791", description: "Psychiatric diagnostic evaluation" },
|
|
473
|
+
{ code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
|
|
474
|
+
{ code: "90832", description: "Psychotherapy, 30 minutes" },
|
|
475
|
+
{ code: "90834", description: "Psychotherapy, 45 minutes" },
|
|
476
|
+
{ code: "90837", description: "Psychotherapy, 60 minutes" },
|
|
477
|
+
{ code: "99203", description: "New patient office visit, level 3" },
|
|
478
|
+
{ code: "99204", description: "New patient office visit, level 4" },
|
|
479
|
+
{ code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
|
|
480
|
+
{ code: "99213", description: "Established patient office visit, level 3" },
|
|
481
|
+
{ code: "99214", description: "Established patient office visit, level 4" },
|
|
482
|
+
{ code: "99215", description: "Established patient office visit, level 5" },
|
|
483
|
+
{ code: "96130", description: "Psychological testing evaluation, first hour" },
|
|
484
|
+
{ code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
|
|
485
|
+
{ code: "97161", description: "Physical therapy evaluation, low complexity" },
|
|
486
|
+
{ code: "97162", description: "Physical therapy evaluation, moderate complexity" },
|
|
487
|
+
{ code: "97163", description: "Physical therapy evaluation, high complexity" },
|
|
488
|
+
{ code: "97110", description: "Therapeutic exercises" },
|
|
489
|
+
{ code: "97140", description: "Manual therapy techniques" },
|
|
490
|
+
{ code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
|
|
491
|
+
{ code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
|
|
492
|
+
{ code: "72141", description: "MRI cervical spine without contrast" },
|
|
493
|
+
{ code: "72148", description: "MRI lumbar spine without contrast" },
|
|
494
|
+
{ code: "73721", description: "MRI lower-extremity joint without contrast" },
|
|
495
|
+
{ code: "20610", description: "Major joint injection or aspiration" },
|
|
496
|
+
{ code: "99070", description: "Supplies and materials" },
|
|
497
|
+
];
|
|
498
|
+
const MODIFIERS = [
|
|
499
|
+
{ code: "92", description: "Primary Treating Physician evaluation" },
|
|
500
|
+
{ code: "93", description: "Interpreter required" },
|
|
501
|
+
{ code: "94", description: "Agreed Medical Evaluator" },
|
|
502
|
+
{ code: "95", description: "Qualified Medical Evaluator" },
|
|
503
|
+
{ code: "96", description: "Psychiatric/psychological evaluation" },
|
|
504
|
+
{ code: "97", description: "Toxicology evaluation" },
|
|
505
|
+
{ code: "98", description: "Oncology evaluation" },
|
|
506
|
+
{ code: "59", description: "Distinct procedural service" },
|
|
507
|
+
{ code: "XE", description: "Separate encounter" },
|
|
508
|
+
{ code: "XS", description: "Separate structure" },
|
|
509
|
+
{ code: "XP", description: "Separate practitioner" },
|
|
510
|
+
{ code: "XU", description: "Unusual non-overlapping service" },
|
|
511
|
+
];
|
|
512
|
+
const TAXONOMIES = [
|
|
513
|
+
{ code: "103G00000X", description: "Clinical Neuropsychologist" },
|
|
514
|
+
{ code: "103T00000X", description: "Psychologist" },
|
|
515
|
+
{ code: "111N00000X", description: "Chiropractor" },
|
|
516
|
+
{ code: "207Q00000X", description: "Family Medicine" },
|
|
517
|
+
{ code: "207R00000X", description: "Internal Medicine" },
|
|
518
|
+
{ code: "207X00000X", description: "Orthopaedic Surgery" },
|
|
519
|
+
{ code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
|
|
520
|
+
{ code: "208100000X", description: "Physical Medicine & Rehabilitation" },
|
|
521
|
+
{ code: "2083X0100X", description: "Occupational Medicine" },
|
|
522
|
+
{ code: "2084N0400X", description: "Neurology" },
|
|
523
|
+
{ code: "2084P0800X", description: "Psychiatry" },
|
|
524
|
+
{ code: "208VP0000X", description: "Pain Medicine" },
|
|
525
|
+
{ code: "213E00000X", description: "Podiatrist" },
|
|
526
|
+
{ code: "225100000X", description: "Physical Therapist" },
|
|
527
|
+
{ code: "225X00000X", description: "Occupational Therapist" },
|
|
528
|
+
{ code: "363A00000X", description: "Physician Assistant" },
|
|
529
|
+
{ code: "363L00000X", description: "Nurse Practitioner" },
|
|
530
|
+
];
|
|
531
|
+
const QUICK_DIAGNOSES = [
|
|
532
|
+
{ label: "Psych", code: "Z04.6", description: "General psychiatric examination requested by authority" },
|
|
533
|
+
{ label: "Back", code: "M54.50", description: "Low back pain, unspecified" },
|
|
534
|
+
{ label: "Neck", code: "M54.2", description: "Cervicalgia" },
|
|
535
|
+
{ label: "Left hand", code: "M79.642", description: "Pain in left hand" },
|
|
536
|
+
{ label: "Right hand", code: "M79.641", description: "Pain in right hand" },
|
|
537
|
+
{ label: "Left knee", code: "M25.562", description: "Pain in left knee" },
|
|
538
|
+
{ label: "Right knee", code: "M25.561", description: "Pain in right knee" },
|
|
539
|
+
];
|
|
475
540
|
const THEME$4 = {
|
|
476
541
|
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" },
|
|
477
542
|
"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" },
|
|
478
543
|
"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" },
|
|
479
|
-
"clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#
|
|
544
|
+
"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" },
|
|
480
545
|
};
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
return Math.max(0, Math.floor(bill.agingDays));
|
|
484
|
-
const source = bill.submittedAt ?? bill.updatedAt;
|
|
485
|
-
if (!source)
|
|
486
|
-
return 0;
|
|
487
|
-
const time = new Date(source).getTime();
|
|
488
|
-
return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
|
|
489
|
-
}
|
|
490
|
-
function mindBillAgingBucket(days) {
|
|
491
|
-
if (days <= 30)
|
|
492
|
-
return "current";
|
|
493
|
-
if (days <= 60)
|
|
494
|
-
return "31-60";
|
|
495
|
-
if (days <= 90)
|
|
496
|
-
return "61-90";
|
|
497
|
-
return "91+";
|
|
498
|
-
}
|
|
499
|
-
function summarizeMindBillDashboard(bills, now = new Date()) {
|
|
500
|
-
const aging = [
|
|
501
|
-
{ id: "current", label: "0–30 days", count: 0, balance: 0 },
|
|
502
|
-
{ id: "31-60", label: "31–60 days", count: 0, balance: 0 },
|
|
503
|
-
{ id: "61-90", label: "61–90 days", count: 0, balance: 0 },
|
|
504
|
-
{ id: "91+", label: "91+ days", count: 0, balance: 0 },
|
|
505
|
-
];
|
|
506
|
-
for (const bill of bills) {
|
|
507
|
-
const bucket = aging.find((item) => item.id === mindBillAgingBucket(mindBillAgingDays(bill, now)));
|
|
508
|
-
bucket.count += 1;
|
|
509
|
-
bucket.balance += bill.balanceDue;
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
totalBilled: bills.reduce((sum, item) => sum + item.totalCharge, 0),
|
|
513
|
-
totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
|
|
514
|
-
outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
|
|
515
|
-
openCount: bills.filter((item) => !/closed|paid|written.off/i.test(item.state)).length,
|
|
516
|
-
submittedThisMonth: bills.filter((item) => sameMonth(item.submittedAt, now)).length,
|
|
517
|
-
closedThisMonth: bills.filter((item) => /closed|paid|written.off/i.test(item.state) && sameMonth(item.updatedAt ?? item.submittedAt, now)).length,
|
|
518
|
-
bills,
|
|
519
|
-
aging,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function sameMonth(value, now) {
|
|
523
|
-
if (!value)
|
|
524
|
-
return false;
|
|
525
|
-
const date = new Date(value);
|
|
526
|
-
return Number.isFinite(date.getTime()) && date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth();
|
|
546
|
+
function deepCopyBill(value) {
|
|
547
|
+
return JSON.parse(JSON.stringify(value));
|
|
527
548
|
}
|
|
528
|
-
function
|
|
529
|
-
|
|
530
|
-
for (const bill of bills) {
|
|
531
|
-
const key = dimension === "payer" ? bill.payerName || "Unassigned payer" : dimension === "aging" ? mindBillAgingBucket(mindBillAgingDays(bill)) : bill.state;
|
|
532
|
-
const label = key.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
|
|
533
|
-
const row = rows.get(key) ?? { key, label, billCount: 0, totalBilled: 0, totalPaid: 0, balanceDue: 0 };
|
|
534
|
-
row.billCount += 1;
|
|
535
|
-
row.totalBilled += bill.totalCharge;
|
|
536
|
-
row.totalPaid += bill.totalPaid;
|
|
537
|
-
row.balanceDue += bill.balanceDue;
|
|
538
|
-
rows.set(key, row);
|
|
539
|
-
}
|
|
540
|
-
return [...rows.values()].sort((a, b) => b.balanceDue - a.balanceDue || a.label.localeCompare(b.label));
|
|
549
|
+
function required(value) {
|
|
550
|
+
return typeof value === "string" ? value.trim().length > 0 : value != null;
|
|
541
551
|
}
|
|
542
|
-
function
|
|
543
|
-
const
|
|
544
|
-
|
|
552
|
+
async function blobToBase64(blob) {
|
|
553
|
+
const buffer = new Uint8Array(await blob.arrayBuffer());
|
|
554
|
+
let binary = "";
|
|
555
|
+
for (const byte of buffer)
|
|
556
|
+
binary += String.fromCharCode(byte);
|
|
557
|
+
return btoa(binary);
|
|
545
558
|
}
|
|
546
|
-
class
|
|
559
|
+
class MindBillBillSubmissionComponent {
|
|
560
|
+
initialBill;
|
|
561
|
+
attachments = [];
|
|
547
562
|
appearance = { preset: "mindbill" };
|
|
563
|
+
sessionEndpoint = "/api/mindbill/session";
|
|
564
|
+
getSession;
|
|
565
|
+
apiBaseUrl;
|
|
566
|
+
heading = "Bill information";
|
|
567
|
+
description = "Review the bill details, add attachments, and submit.";
|
|
568
|
+
submitLabel = "Submit bill";
|
|
569
|
+
attentionFields = [];
|
|
570
|
+
procedureOptions = PROCEDURES;
|
|
571
|
+
modifierOptions = MODIFIERS;
|
|
572
|
+
taxonomyOptions = TAXONOMIES;
|
|
573
|
+
submitter;
|
|
574
|
+
submitted = new EventEmitter();
|
|
575
|
+
submissionError = new EventEmitter();
|
|
576
|
+
billChange = new EventEmitter();
|
|
577
|
+
bill = {};
|
|
578
|
+
workingAttachments = [];
|
|
579
|
+
payerQuery = "";
|
|
580
|
+
payerResults = [];
|
|
581
|
+
payerBusy = false;
|
|
582
|
+
diagnosisQuery = "";
|
|
583
|
+
diagnosisResults = [];
|
|
584
|
+
diagnosisOffset = 0;
|
|
585
|
+
diagnosisBusy = false;
|
|
586
|
+
submitting = false;
|
|
587
|
+
errorMessage = "";
|
|
588
|
+
postalStatus = "";
|
|
589
|
+
dragActive = false;
|
|
590
|
+
invalidFields = new Set();
|
|
591
|
+
attentionFieldSet = new Set();
|
|
592
|
+
quickDiagnoses = QUICK_DIAGNOSES;
|
|
593
|
+
// Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
|
|
594
|
+
// must schedule change detection explicitly for zone-less Angular hosts.
|
|
595
|
+
changeDetector = inject(ChangeDetectorRef);
|
|
596
|
+
customProcedureOption = mindBillCustomProcedureOption;
|
|
597
|
+
customTaxonomyOption = (query) => {
|
|
598
|
+
const code = query.trim().toUpperCase();
|
|
599
|
+
return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
|
|
600
|
+
};
|
|
601
|
+
searchTimer;
|
|
602
|
+
postalTimer;
|
|
603
|
+
ngOnChanges(changes) {
|
|
604
|
+
if (changes["initialBill"] && this.initialBill) {
|
|
605
|
+
this.bill = deepCopyBill(this.initialBill);
|
|
606
|
+
if (!this.bill.serviceLines.length)
|
|
607
|
+
this.bill.serviceLines = [{ code: "", units: 1 }];
|
|
608
|
+
this.ensureTrailingLine();
|
|
609
|
+
this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
|
|
610
|
+
}
|
|
611
|
+
if (changes["attachments"])
|
|
612
|
+
this.workingAttachments = this.attachments.map((item) => ({ ...item }));
|
|
613
|
+
if (changes["attentionFields"])
|
|
614
|
+
this.attentionFieldSet = new Set(this.attentionFields);
|
|
615
|
+
}
|
|
548
616
|
get themeStyle() {
|
|
549
617
|
const base = THEME$4[this.appearance.preset ?? "mindbill"] ?? THEME$4["mindbill"];
|
|
550
618
|
return {
|
|
@@ -555,885 +623,1229 @@ class DashboardBase {
|
|
|
555
623
|
"--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
|
|
556
624
|
};
|
|
557
625
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
626
|
+
get procedureComboOptions() {
|
|
627
|
+
return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
|
|
628
|
+
}
|
|
629
|
+
get taxonomyComboOptions() {
|
|
630
|
+
return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
|
|
631
|
+
}
|
|
632
|
+
modifierComboOptions(line) {
|
|
633
|
+
return this.modifierOptions
|
|
634
|
+
.filter((item) => !(line.modifiers ?? []).includes(item.code))
|
|
635
|
+
.map((item) => ({ id: item.code, label: `−${item.code}`, detail: item.description }));
|
|
636
|
+
}
|
|
637
|
+
referenceClient() {
|
|
638
|
+
return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
|
|
639
|
+
}
|
|
640
|
+
setDate(path, value) {
|
|
641
|
+
if (path === "patient.dateOfBirth")
|
|
642
|
+
this.bill.patient.dateOfBirth = value;
|
|
643
|
+
else if (path === "service.date")
|
|
644
|
+
this.bill.service.date = value;
|
|
645
|
+
else
|
|
646
|
+
this.bill.claim.dateOfInjury = value;
|
|
647
|
+
if (value)
|
|
648
|
+
this.invalidFields.delete(path);
|
|
649
|
+
this.billChange.emit(this.bill);
|
|
650
|
+
}
|
|
651
|
+
postalChanged(value) {
|
|
652
|
+
clearTimeout(this.postalTimer);
|
|
653
|
+
this.postalStatus = "";
|
|
654
|
+
if (!/^\d{5}$/.test(value.trim()))
|
|
655
|
+
return;
|
|
656
|
+
this.postalStatus = "Looking up ZIP…";
|
|
657
|
+
this.postalTimer = setTimeout(async () => {
|
|
658
|
+
try {
|
|
659
|
+
const place = await this.referenceClient().lookupPostalCode(value);
|
|
660
|
+
if (place) {
|
|
661
|
+
this.bill.patient.address.city = place.city;
|
|
662
|
+
this.bill.patient.address.state = place.state.toUpperCase();
|
|
663
|
+
this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
|
|
664
|
+
this.invalidFields.delete("patient.address.city");
|
|
665
|
+
this.invalidFields.delete("patient.address.state");
|
|
666
|
+
this.billChange.emit(this.bill);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
this.postalStatus = "ZIP not found";
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
this.postalStatus = "ZIP lookup unavailable";
|
|
674
|
+
}
|
|
675
|
+
this.changeDetector.markForCheck();
|
|
676
|
+
}, 180);
|
|
677
|
+
}
|
|
678
|
+
searchPayers() {
|
|
679
|
+
clearTimeout(this.searchTimer);
|
|
680
|
+
this.searchTimer = setTimeout(async () => {
|
|
681
|
+
this.payerBusy = true;
|
|
682
|
+
try {
|
|
683
|
+
this.payerResults = await this.referenceClient().searchClaimsAdministrators(this.payerQuery, this.bill.claim.claimNumber);
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
this.payerResults = [];
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
this.payerBusy = false;
|
|
690
|
+
this.changeDetector.markForCheck();
|
|
691
|
+
}
|
|
692
|
+
}, 180);
|
|
693
|
+
}
|
|
694
|
+
selectPayer(payer) {
|
|
695
|
+
this.bill.claim.claimsAdministrator = { id: payer.id, name: payer.name };
|
|
696
|
+
this.payerQuery = payer.name;
|
|
697
|
+
this.payerResults = [];
|
|
698
|
+
this.invalidFields.delete("claim.claimsAdministrator");
|
|
699
|
+
this.billChange.emit(this.bill);
|
|
700
|
+
}
|
|
701
|
+
searchDiagnoses(reset) {
|
|
702
|
+
if (this.diagnosisBusy)
|
|
703
|
+
return;
|
|
704
|
+
if (reset) {
|
|
705
|
+
this.diagnosisOffset = 0;
|
|
706
|
+
this.diagnosisResults = [];
|
|
707
|
+
}
|
|
708
|
+
clearTimeout(this.searchTimer);
|
|
709
|
+
this.searchTimer = setTimeout(async () => {
|
|
710
|
+
this.diagnosisBusy = true;
|
|
711
|
+
try {
|
|
712
|
+
const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
|
|
713
|
+
this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
|
|
714
|
+
this.diagnosisOffset = this.diagnosisResults.length;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
if (reset)
|
|
718
|
+
this.diagnosisResults = [];
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
this.diagnosisBusy = false;
|
|
722
|
+
this.changeDetector.markForCheck();
|
|
723
|
+
}
|
|
724
|
+
}, reset ? 160 : 0);
|
|
725
|
+
}
|
|
726
|
+
diagnosisScrolled(event) {
|
|
727
|
+
const element = event.currentTarget;
|
|
728
|
+
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
|
|
729
|
+
this.searchDiagnoses(false);
|
|
730
|
+
}
|
|
731
|
+
hasDiagnosis(code) { return this.bill.diagnoses.includes(code); }
|
|
732
|
+
toggleDiagnosis(item) {
|
|
733
|
+
if (this.hasDiagnosis(item.code))
|
|
734
|
+
this.removeDiagnosis(item.code);
|
|
735
|
+
else
|
|
736
|
+
this.addDiagnosis(item);
|
|
737
|
+
}
|
|
738
|
+
addDiagnosis(item) { if (!this.hasDiagnosis(item.code))
|
|
739
|
+
this.bill.diagnoses.push(item.code); this.diagnosisQuery = ""; this.diagnosisResults = []; this.invalidFields.delete("diagnoses"); this.billChange.emit(this.bill); }
|
|
740
|
+
removeDiagnosis(code) { this.bill.diagnoses = this.bill.diagnoses.filter((item) => item !== code); this.billChange.emit(this.bill); }
|
|
741
|
+
ensureTrailingLine() {
|
|
742
|
+
const last = this.bill.serviceLines.at(-1);
|
|
743
|
+
if (!last || last.code.trim())
|
|
744
|
+
this.bill.serviceLines.push({ code: "", units: 1 });
|
|
745
|
+
this.billChange.emit(this.bill);
|
|
746
|
+
}
|
|
747
|
+
removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
|
|
748
|
+
this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
|
|
749
|
+
selectProcedure(index, option) {
|
|
750
|
+
this.bill.serviceLines[index].code = option.id;
|
|
751
|
+
this.invalidFields.delete("serviceLines");
|
|
752
|
+
this.ensureTrailingLine();
|
|
753
|
+
}
|
|
754
|
+
addModifier(index, option) {
|
|
755
|
+
const line = this.bill.serviceLines[index];
|
|
756
|
+
line.modifiers = [...new Set([...(line.modifiers ?? []), option.id])];
|
|
757
|
+
this.billChange.emit(this.bill);
|
|
758
|
+
}
|
|
759
|
+
removeModifier(index, modifier) {
|
|
760
|
+
const line = this.bill.serviceLines[index];
|
|
761
|
+
line.modifiers = (line.modifiers ?? []).filter((item) => item !== modifier);
|
|
762
|
+
this.billChange.emit(this.bill);
|
|
763
|
+
}
|
|
764
|
+
dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
|
|
765
|
+
hasDiagnosisPointer(line, pointer) {
|
|
766
|
+
return (line.diagnosisPointers ?? []).includes(pointer);
|
|
767
|
+
}
|
|
768
|
+
toggleDiagnosisPointer(index, pointer) {
|
|
769
|
+
const line = this.bill.serviceLines[index];
|
|
770
|
+
line.diagnosisPointers = this.hasDiagnosisPointer(line, pointer)
|
|
771
|
+
? (line.diagnosisPointers ?? []).filter((item) => item !== pointer)
|
|
772
|
+
: [...(line.diagnosisPointers ?? []), pointer].sort((left, right) => left - right);
|
|
773
|
+
this.billChange.emit(this.bill);
|
|
774
|
+
}
|
|
775
|
+
setAuthorizationNumber(value) {
|
|
776
|
+
this.bill.service.authorizationNumber = value.trim() ? value : null;
|
|
777
|
+
this.billChange.emit(this.bill);
|
|
778
|
+
}
|
|
779
|
+
selectTaxonomy(option) {
|
|
780
|
+
this.bill.renderingProvider.taxonomy = option.id;
|
|
781
|
+
this.invalidFields.delete("renderingProvider.taxonomy");
|
|
782
|
+
this.billChange.emit(this.bill);
|
|
783
|
+
}
|
|
784
|
+
procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
|
|
785
|
+
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; }
|
|
786
|
+
get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
|
|
787
|
+
onWindowDragOver(event) {
|
|
788
|
+
if (event.dataTransfer?.types.includes("Files")) {
|
|
789
|
+
event.preventDefault();
|
|
790
|
+
this.dragActive = true;
|
|
791
|
+
this.changeDetector.markForCheck();
|
|
699
792
|
}
|
|
700
793
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const header = ["Status", ...MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => bucket.label), "Total"];
|
|
707
|
-
const lines = matrix.rows.map((row) => [row.label, ...row.cells.map((cell) => cell.count), row.total.count]);
|
|
708
|
-
lines.push(["Total", ...matrix.columnTotals.map((cell) => cell.count), matrix.grandTotal.count]);
|
|
709
|
-
return [header, ...lines].map((line) => line.map(quote).join(",")).join("\n");
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const THEME$3 = {
|
|
713
|
-
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" },
|
|
714
|
-
"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" },
|
|
715
|
-
"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" },
|
|
716
|
-
"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" },
|
|
717
|
-
};
|
|
718
|
-
class MindBillStatusAgingMatrixComponent {
|
|
719
|
-
bills = [];
|
|
720
|
-
heading = "Bills by status and age";
|
|
721
|
-
description = "Every bill grouped by lifecycle status, then by days outstanding.";
|
|
722
|
-
stateOrder;
|
|
723
|
-
showBalances = true;
|
|
724
|
-
appearance = { preset: "mindbill" };
|
|
725
|
-
cellSelected = new EventEmitter();
|
|
726
|
-
buckets = MINDBILL_STATUS_AGING_BUCKETS;
|
|
727
|
-
get matrix() {
|
|
728
|
-
return buildMindBillStatusAgingMatrix(this.bills, this.stateOrder);
|
|
794
|
+
onWindowDragLeave(event) {
|
|
795
|
+
if (!event.relatedTarget) {
|
|
796
|
+
this.dragActive = false;
|
|
797
|
+
this.changeDetector.markForCheck();
|
|
798
|
+
}
|
|
729
799
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
"--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
|
|
738
|
-
};
|
|
800
|
+
onWindowDrop(event) {
|
|
801
|
+
if (event.dataTransfer?.files.length) {
|
|
802
|
+
event.preventDefault();
|
|
803
|
+
this.dragActive = false;
|
|
804
|
+
void this.addFiles(event.dataTransfer.files);
|
|
805
|
+
this.changeDetector.markForCheck();
|
|
806
|
+
}
|
|
739
807
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
</table>
|
|
768
|
-
</div>
|
|
769
|
-
</section>
|
|
770
|
-
`, isInline: true, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
|
|
771
|
-
}
|
|
772
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, decorators: [{
|
|
773
|
-
type: Component,
|
|
774
|
-
args: [{ selector: "mindbill-status-aging-matrix", standalone: true, imports: [CommonModule], template: `
|
|
775
|
-
<section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
|
|
776
|
-
<header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
|
|
777
|
-
<div class="card">
|
|
778
|
-
<table>
|
|
779
|
-
<thead>
|
|
780
|
-
<tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
|
|
781
|
-
</thead>
|
|
782
|
-
<tbody>
|
|
783
|
-
@for (row of matrix.rows; track row.state) {
|
|
784
|
-
<tr>
|
|
785
|
-
<td class="status"><i class="pill">{{ row.label }}</i></td>
|
|
786
|
-
@for (cell of row.cells; track cell.bucket) {
|
|
787
|
-
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
788
|
-
}
|
|
789
|
-
<td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
|
|
790
|
-
</tr>
|
|
791
|
-
}
|
|
792
|
-
<tr class="totals">
|
|
793
|
-
<td class="status">Total</td>
|
|
794
|
-
@for (cell of matrix.columnTotals; track cell.bucket) {
|
|
795
|
-
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
796
|
-
}
|
|
797
|
-
<td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
|
|
798
|
-
</tr>
|
|
799
|
-
</tbody>
|
|
800
|
-
</table>
|
|
801
|
-
</div>
|
|
802
|
-
</section>
|
|
803
|
-
`, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"] }]
|
|
804
|
-
}], propDecorators: { bills: [{
|
|
805
|
-
type: Input
|
|
806
|
-
}], heading: [{
|
|
807
|
-
type: Input
|
|
808
|
-
}], description: [{
|
|
809
|
-
type: Input
|
|
810
|
-
}], stateOrder: [{
|
|
811
|
-
type: Input
|
|
812
|
-
}], showBalances: [{
|
|
813
|
-
type: Input
|
|
814
|
-
}], appearance: [{
|
|
815
|
-
type: Input
|
|
816
|
-
}], cellSelected: [{
|
|
817
|
-
type: Output
|
|
818
|
-
}] } });
|
|
819
|
-
|
|
820
|
-
const MINDBILL_BILL_TASKS_TONES = {
|
|
821
|
-
violet: "#7c53c3",
|
|
822
|
-
red: "#cf4437",
|
|
823
|
-
blue: "#2f7fd1",
|
|
824
|
-
green: "#2c9a5b",
|
|
825
|
-
amber: "#d9931f",
|
|
826
|
-
neutral: "#8195a1",
|
|
827
|
-
};
|
|
828
|
-
const MINDBILL_BILL_TASKS_PILL_BASES = ["#2c9a5b", "#1f9aa8", "#dcbb28", "#e08a2e", "#d75d8a"];
|
|
829
|
-
function mindBillBillTasksTone(tone) {
|
|
830
|
-
return MINDBILL_BILL_TASKS_TONES[tone] ?? MINDBILL_BILL_TASKS_TONES.neutral;
|
|
831
|
-
}
|
|
832
|
-
function mindBillBillTasksPillBase(index) {
|
|
833
|
-
return MINDBILL_BILL_TASKS_PILL_BASES[index % MINDBILL_BILL_TASKS_PILL_BASES.length];
|
|
834
|
-
}
|
|
835
|
-
/** Click-through payload for a bucket cell (bucketIndex) or a row total (null). */
|
|
836
|
-
function mindBillBillTasksCell(section, row, bucketIndex, buckets) {
|
|
837
|
-
if (bucketIndex === null) {
|
|
838
|
-
return {
|
|
839
|
-
sectionId: section.id,
|
|
840
|
-
rowId: row.id,
|
|
841
|
-
bucketId: null,
|
|
842
|
-
refs: row.refs.flat(),
|
|
843
|
-
count: row.total,
|
|
844
|
-
};
|
|
808
|
+
async addFiles(fileList) {
|
|
809
|
+
const files = Array.from(fileList);
|
|
810
|
+
if (!files.length)
|
|
811
|
+
return;
|
|
812
|
+
this.errorMessage = "";
|
|
813
|
+
const invalid = files.find((file) => file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf"));
|
|
814
|
+
if (invalid) {
|
|
815
|
+
this.errorMessage = `${invalid.name} is not a PDF.`;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const oversized = files.find((file) => file.size > MAX_PDF_BYTES);
|
|
819
|
+
if (oversized) {
|
|
820
|
+
this.errorMessage = `${oversized.name} is larger than 25 MB.`;
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (this.workingAttachments.length + files.length > MAX_DOCUMENTS) {
|
|
824
|
+
this.errorMessage = `A bill can include at most ${MAX_DOCUMENTS} attachments.`;
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const existingBytes = this.workingAttachments.reduce((sum, item) => sum + (item.contentBase64 ? item.contentBase64.length * 0.75 : 0), 0);
|
|
828
|
+
if (existingBytes + files.reduce((sum, file) => sum + file.size, 0) > MAX_UPLOAD_BYTES) {
|
|
829
|
+
this.errorMessage = "Attachments exceed the 100 MB upload limit.";
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
for (const file of files)
|
|
833
|
+
this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
|
|
834
|
+
this.changeDetector.markForCheck();
|
|
845
835
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
refs: row.refs[bucketIndex] ?? [],
|
|
851
|
-
count: row.counts[bucketIndex] ?? 0,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// daisyBill-style "Bill Tasks" dashboard: one rounded, tone-colored card per
|
|
856
|
-
// task section, rows bucketed by age in days with colored bucket header pills,
|
|
857
|
-
// per-cell click-through counts, and a closing grand-total card. The pure
|
|
858
|
-
// aggregation lives in @mindbill/browser (buildBillTasksDashboard) so React
|
|
859
|
-
// and Angular render the exact same data shape.
|
|
860
|
-
class MindBillBillTasksDashboardComponent {
|
|
861
|
-
data = null;
|
|
862
|
-
buckets = BILL_TASKS_AGING_BUCKETS;
|
|
863
|
-
heading = "";
|
|
864
|
-
grandTotalLabel = "Bill Tasks Total";
|
|
865
|
-
emptyLabel = "No Tasks";
|
|
866
|
-
appearance = { preset: "mindbill" };
|
|
867
|
-
cellSelected = new EventEmitter();
|
|
868
|
-
get themeStyle() {
|
|
869
|
-
return {
|
|
870
|
-
...mindBillAngularAppearanceStyle(this.appearance),
|
|
871
|
-
"--cols": `minmax(150px,1.6fr) repeat(${this.buckets.length},minmax(84px,1fr)) minmax(84px,1fr)`,
|
|
872
|
-
};
|
|
836
|
+
async filesSelected(event) {
|
|
837
|
+
const input = event.target;
|
|
838
|
+
await this.addFiles(input.files ?? []);
|
|
839
|
+
input.value = "";
|
|
873
840
|
}
|
|
874
|
-
|
|
875
|
-
|
|
841
|
+
removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
|
|
842
|
+
URL.revokeObjectURL(item.contentUrl); this.workingAttachments.splice(index, 1); }
|
|
843
|
+
async previewAttachment(attachment) {
|
|
844
|
+
if (attachment.loadBlob) {
|
|
845
|
+
const url = URL.createObjectURL(await attachment.loadBlob());
|
|
846
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
847
|
+
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
|
848
|
+
}
|
|
849
|
+
else if (attachment.contentUrl)
|
|
850
|
+
window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
|
|
851
|
+
else if (attachment.contentBase64)
|
|
852
|
+
window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer");
|
|
876
853
|
}
|
|
877
|
-
|
|
878
|
-
|
|
854
|
+
bad(path) { return this.invalidFields.has(path) || this.attentionFieldSet.has(path); }
|
|
855
|
+
validate() {
|
|
856
|
+
const candidates = [
|
|
857
|
+
["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
|
|
858
|
+
["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],
|
|
859
|
+
["service.date", parseMindBillSubmissionDate(this.bill.service.date ?? "")], ["claim.dateOfInjury", parseMindBillSubmissionDate(this.bill.claim.dateOfInjury ?? "")], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
|
|
860
|
+
["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
|
|
861
|
+
["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
|
|
862
|
+
["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],
|
|
863
|
+
["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
|
|
864
|
+
["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],
|
|
865
|
+
["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
|
|
866
|
+
];
|
|
867
|
+
this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
|
|
868
|
+
if (this.invalidFields.size) {
|
|
869
|
+
this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
|
|
870
|
+
setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
return true;
|
|
879
874
|
}
|
|
880
|
-
|
|
881
|
-
|
|
875
|
+
async submissionDocuments() {
|
|
876
|
+
const documents = [];
|
|
877
|
+
for (const attachment of this.workingAttachments) {
|
|
878
|
+
let contentBase64 = attachment.contentBase64;
|
|
879
|
+
if (!contentBase64 && attachment.loadBlob)
|
|
880
|
+
contentBase64 = await blobToBase64(await attachment.loadBlob());
|
|
881
|
+
if (!contentBase64 && attachment.contentUrl) {
|
|
882
|
+
const response = await fetch(attachment.contentUrl);
|
|
883
|
+
if (!response.ok)
|
|
884
|
+
throw new Error(`Could not load ${attachment.filename}.`);
|
|
885
|
+
contentBase64 = await blobToBase64(await response.blob());
|
|
886
|
+
}
|
|
887
|
+
if (!contentBase64)
|
|
888
|
+
continue;
|
|
889
|
+
documents.push({
|
|
890
|
+
filename: attachment.filename,
|
|
891
|
+
documentType: attachment.documentType,
|
|
892
|
+
contentBase64,
|
|
893
|
+
...(attachment.externalId ? { externalId: attachment.externalId } : {}),
|
|
894
|
+
...(attachment.description ? { description: attachment.description } : {}),
|
|
895
|
+
...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return documents;
|
|
882
899
|
}
|
|
883
|
-
|
|
884
|
-
|
|
900
|
+
async submit(event) {
|
|
901
|
+
event.preventDefault();
|
|
902
|
+
this.errorMessage = "";
|
|
903
|
+
if (!this.validate())
|
|
904
|
+
return;
|
|
905
|
+
this.submitting = true;
|
|
906
|
+
try {
|
|
907
|
+
const bill = deepCopyBill(this.bill);
|
|
908
|
+
bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
|
|
909
|
+
bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
|
|
910
|
+
bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
|
|
911
|
+
bill.serviceLines = bill.serviceLines
|
|
912
|
+
.filter((line) => line.code.trim())
|
|
913
|
+
.map((line) => {
|
|
914
|
+
const charge = line.charge ?? this.allowed(line);
|
|
915
|
+
return charge == null ? line : { ...line, charge };
|
|
916
|
+
});
|
|
917
|
+
const input = { bill, documents: await this.submissionDocuments() };
|
|
918
|
+
const result = this.submitter
|
|
919
|
+
? await this.submitter(input)
|
|
920
|
+
: await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
|
|
921
|
+
this.submitted.emit(result);
|
|
922
|
+
}
|
|
923
|
+
catch (value) {
|
|
924
|
+
const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
|
|
925
|
+
this.errorMessage = error.message;
|
|
926
|
+
this.submissionError.emit(error);
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
this.submitting = false;
|
|
930
|
+
this.changeDetector.markForCheck();
|
|
931
|
+
}
|
|
885
932
|
}
|
|
886
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
887
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type:
|
|
888
|
-
<
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
933
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
934
|
+
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", attentionFields: "attentionFields", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, host: { listeners: { "window:dragover": "onWindowDragOver($event)", "window:dragleave": "onWindowDragLeave($event)", "window:drop": "onWindowDrop($event)" } }, usesOnChanges: true, ngImport: i0, template: `
|
|
935
|
+
<form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
|
|
936
|
+
<div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
|
|
937
|
+
|
|
938
|
+
<fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
|
|
939
|
+
<label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
|
|
940
|
+
<label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
|
|
941
|
+
<label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
|
|
942
|
+
<label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
|
|
943
|
+
<label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
|
|
944
|
+
<label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
|
|
945
|
+
<label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
|
|
946
|
+
<label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
|
|
947
|
+
</div></fieldset>
|
|
948
|
+
|
|
949
|
+
<fieldset id="mbs-claim"><legend>Injury & claim</legend><div class="grid">
|
|
950
|
+
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
|
|
951
|
+
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
|
|
952
|
+
<label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
|
|
953
|
+
<label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
|
|
954
|
+
<label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
|
|
955
|
+
<div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
|
|
956
|
+
<input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
|
|
957
|
+
@if (payerBusy) { <small>Searching directory…</small> }
|
|
958
|
+
@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> }
|
|
959
|
+
@if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
|
|
960
|
+
</div>
|
|
961
|
+
<label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
|
|
962
|
+
<div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
|
|
963
|
+
<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>
|
|
964
|
+
@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> }
|
|
965
|
+
<input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
|
|
966
|
+
@if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
|
|
967
|
+
@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> }
|
|
968
|
+
</div>
|
|
969
|
+
</div></fieldset>
|
|
970
|
+
|
|
971
|
+
<fieldset id="mbs-provider"><legend>Providers & place of service</legend><div class="grid">
|
|
972
|
+
<h3 class="wide">Billing provider</h3>
|
|
973
|
+
<label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
|
|
974
|
+
<label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
|
|
975
|
+
<label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
976
|
+
<label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
|
|
977
|
+
<label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
|
|
978
|
+
<label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
|
|
979
|
+
<label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
|
|
980
|
+
<label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
|
|
981
|
+
<h3 class="wide">Rendering provider</h3>
|
|
982
|
+
<label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
|
|
983
|
+
<label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
984
|
+
<div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
|
|
985
|
+
<mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
|
|
986
|
+
<small>Search by specialty name or 10-character taxonomy code.</small>
|
|
987
|
+
</div>
|
|
988
|
+
<h3 class="wide">Place of service</h3>
|
|
989
|
+
<label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
|
|
990
|
+
<label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
|
|
991
|
+
<label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
|
|
992
|
+
<label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
|
|
993
|
+
<label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
|
|
994
|
+
</div></fieldset>
|
|
995
|
+
|
|
996
|
+
<fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
|
|
997
|
+
<div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
|
|
998
|
+
@for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
|
|
999
|
+
<div class="cell"><span class="mobile-label">Procedure</span>
|
|
1000
|
+
<mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
|
|
1001
|
+
@if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
|
|
901
1002
|
</div>
|
|
902
|
-
|
|
903
|
-
<
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
<button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
|
|
912
|
-
} @else { <span class="zero">0</span> }
|
|
913
|
-
</span>
|
|
1003
|
+
<div class="cell"><span class="mobile-label">Modifiers</span>
|
|
1004
|
+
@if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
|
|
1005
|
+
<mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="cell"><span class="mobile-label">Dx pointers</span>
|
|
1008
|
+
<div class="dx">
|
|
1009
|
+
@if (bill.diagnoses.length) {
|
|
1010
|
+
@for (code of bill.diagnoses; track code; let dxIndex = $index) {
|
|
1011
|
+
<button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
|
|
914
1012
|
}
|
|
915
|
-
|
|
916
|
-
@if (row.total > 0) {
|
|
917
|
-
<button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
|
|
918
|
-
} @else { <span class="zero">0</span> }
|
|
919
|
-
</span>
|
|
920
|
-
</div>
|
|
921
|
-
}
|
|
922
|
-
<div class="grid row totals">
|
|
923
|
-
<span class="rowlabel">Total</span>
|
|
924
|
-
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
925
|
-
<span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
|
|
926
|
-
}
|
|
927
|
-
<span class="total">{{ section.total }}</span>
|
|
1013
|
+
} @else { <small>Add diagnoses above</small> }
|
|
928
1014
|
</div>
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1015
|
+
</div>
|
|
1016
|
+
<label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
|
|
1017
|
+
<strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
|
|
1018
|
+
<button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
|
|
1019
|
+
</div> }
|
|
1020
|
+
<div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
|
|
1021
|
+
</fieldset>
|
|
1022
|
+
|
|
1023
|
+
<fieldset><legend>Attachments</legend>
|
|
1024
|
+
@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> }
|
|
1025
|
+
<label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
|
|
1026
|
+
</fieldset>
|
|
1027
|
+
|
|
1028
|
+
@if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
|
|
1029
|
+
<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>
|
|
1030
|
+
@if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
|
|
1031
|
+
</form>
|
|
1032
|
+
`, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MindBillComboBoxComponent, selector: "mindbill-combo-box", inputs: ["value", "placeholder", "options", "disabled", "loading", "loadingMore", "invalid", "preserveValueOnOpen", "ariaLabel", "createOption"], outputs: ["opened", "queryChange", "endReached", "selected"] }, { kind: "component", type: MindBillDateInputComponent, selector: "mindbill-date-input", inputs: ["value", "disabled", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
|
|
1033
|
+
}
|
|
1034
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
|
|
1035
|
+
type: Component,
|
|
1036
|
+
args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule, MindBillComboBoxComponent, MindBillDateInputComponent], template: `
|
|
1037
|
+
<form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
|
|
1038
|
+
<div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
|
|
1039
|
+
|
|
1040
|
+
<fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
|
|
1041
|
+
<label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
|
|
1042
|
+
<label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
|
|
1043
|
+
<label [class.invalid]="bad('patient.dateOfBirth')">Date of birth <b>*</b><mindbill-date-input ariaLabel="Date of birth" [value]="bill.patient.dateOfBirth" (valueChange)="setDate('patient.dateOfBirth', $event)"/></label>
|
|
1044
|
+
<label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
|
|
1045
|
+
<label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
|
|
1046
|
+
<label [class.invalid]="bad('patient.address.postalCode')">ZIP <b>*</b><input name="zip" [(ngModel)]="bill.patient.address.postalCode" (ngModelChange)="postalChanged($event)" inputmode="numeric">@if (postalStatus) { <small>{{ postalStatus }}</small> }</label>
|
|
1047
|
+
<label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
|
|
1048
|
+
<label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
|
|
1049
|
+
</div></fieldset>
|
|
1050
|
+
|
|
1051
|
+
<fieldset id="mbs-claim"><legend>Injury & claim</legend><div class="grid">
|
|
1052
|
+
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
|
|
1053
|
+
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
|
|
1054
|
+
<label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
|
|
1055
|
+
<label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
|
|
1056
|
+
<label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
|
|
1057
|
+
<div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
|
|
1058
|
+
<input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
|
|
1059
|
+
@if (payerBusy) { <small>Searching directory…</small> }
|
|
1060
|
+
@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> }
|
|
1061
|
+
@if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
|
|
1062
|
+
</div>
|
|
1063
|
+
<label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
|
|
1064
|
+
<div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
|
|
1065
|
+
<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>
|
|
1066
|
+
@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> }
|
|
1067
|
+
<input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
|
|
1068
|
+
@if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
|
|
1069
|
+
@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> }
|
|
1070
|
+
</div>
|
|
1071
|
+
</div></fieldset>
|
|
1072
|
+
|
|
1073
|
+
<fieldset id="mbs-provider"><legend>Providers & place of service</legend><div class="grid">
|
|
1074
|
+
<h3 class="wide">Billing provider</h3>
|
|
1075
|
+
<label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
|
|
1076
|
+
<label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
|
|
1077
|
+
<label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1078
|
+
<label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
|
|
1079
|
+
<label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
|
|
1080
|
+
<label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
|
|
1081
|
+
<label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
|
|
1082
|
+
<label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
|
|
1083
|
+
<h3 class="wide">Rendering provider</h3>
|
|
1084
|
+
<label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
|
|
1085
|
+
<label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1086
|
+
<div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
|
|
1087
|
+
<mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
|
|
1088
|
+
<small>Search by specialty name or 10-character taxonomy code.</small>
|
|
939
1089
|
</div>
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
<div class="
|
|
952
|
-
<
|
|
953
|
-
|
|
954
|
-
<span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
|
|
955
|
-
</span>
|
|
956
|
-
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
957
|
-
<span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
|
|
958
|
-
}
|
|
959
|
-
<span class="colhead">Task Total</span>
|
|
1090
|
+
<h3 class="wide">Place of service</h3>
|
|
1091
|
+
<label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
|
|
1092
|
+
<label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
|
|
1093
|
+
<label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
|
|
1094
|
+
<label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
|
|
1095
|
+
<label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
|
|
1096
|
+
</div></fieldset>
|
|
1097
|
+
|
|
1098
|
+
<fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
|
|
1099
|
+
<div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
|
|
1100
|
+
@for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
|
|
1101
|
+
<div class="cell"><span class="mobile-label">Procedure</span>
|
|
1102
|
+
<mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
|
|
1103
|
+
@if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
|
|
960
1104
|
</div>
|
|
961
|
-
|
|
962
|
-
<
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
<button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
|
|
971
|
-
} @else { <span class="zero">0</span> }
|
|
972
|
-
</span>
|
|
1105
|
+
<div class="cell"><span class="mobile-label">Modifiers</span>
|
|
1106
|
+
@if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
|
|
1107
|
+
<mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
|
|
1108
|
+
</div>
|
|
1109
|
+
<div class="cell"><span class="mobile-label">Dx pointers</span>
|
|
1110
|
+
<div class="dx">
|
|
1111
|
+
@if (bill.diagnoses.length) {
|
|
1112
|
+
@for (code of bill.diagnoses; track code; let dxIndex = $index) {
|
|
1113
|
+
<button type="button" [class.on]="hasDiagnosisPointer(line, dxIndex + 1)" [title]="dxLetter(dxIndex + 1) + ' — ' + code" [attr.aria-pressed]="hasDiagnosisPointer(line, dxIndex + 1)" [attr.aria-label]="'Point line ' + (index + 1) + ' at diagnosis ' + code" (click)="toggleDiagnosisPointer(index, dxIndex + 1)">{{ dxLetter(dxIndex + 1) }}</button>
|
|
973
1114
|
}
|
|
974
|
-
|
|
975
|
-
@if (row.total > 0) {
|
|
976
|
-
<button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
|
|
977
|
-
} @else { <span class="zero">0</span> }
|
|
978
|
-
</span>
|
|
979
|
-
</div>
|
|
980
|
-
}
|
|
981
|
-
<div class="grid row totals">
|
|
982
|
-
<span class="rowlabel">Total</span>
|
|
983
|
-
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
984
|
-
<span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
|
|
985
|
-
}
|
|
986
|
-
<span class="total">{{ section.total }}</span>
|
|
1115
|
+
} @else { <small>Add diagnoses above</small> }
|
|
987
1116
|
</div>
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
</
|
|
999
|
-
</
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1117
|
+
</div>
|
|
1118
|
+
<label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
|
|
1119
|
+
<strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
|
|
1120
|
+
<button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
|
|
1121
|
+
</div> }
|
|
1122
|
+
<div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
|
|
1123
|
+
</fieldset>
|
|
1124
|
+
|
|
1125
|
+
<fieldset><legend>Attachments</legend>
|
|
1126
|
+
@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> }
|
|
1127
|
+
<label class="dropzone" [class.drag-active]="dragActive"><input type="file" accept="application/pdf,.pdf" multiple (change)="filesSelected($event)"><strong>Drop additional PDF files here, or click to choose</strong><span>Add supporting documents by dropping them anywhere on this screen.</span></label>
|
|
1128
|
+
</fieldset>
|
|
1129
|
+
|
|
1130
|
+
@if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
|
|
1131
|
+
<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>
|
|
1132
|
+
@if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
|
|
1133
|
+
</form>
|
|
1134
|
+
`, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
|
|
1135
|
+
}], propDecorators: { initialBill: [{
|
|
1003
1136
|
type: Input
|
|
1004
|
-
}],
|
|
1137
|
+
}], attachments: [{
|
|
1138
|
+
type: Input
|
|
1139
|
+
}], appearance: [{
|
|
1140
|
+
type: Input
|
|
1141
|
+
}], sessionEndpoint: [{
|
|
1142
|
+
type: Input
|
|
1143
|
+
}], getSession: [{
|
|
1144
|
+
type: Input
|
|
1145
|
+
}], apiBaseUrl: [{
|
|
1005
1146
|
type: Input
|
|
1006
1147
|
}], heading: [{
|
|
1007
1148
|
type: Input
|
|
1008
|
-
}],
|
|
1149
|
+
}], description: [{
|
|
1009
1150
|
type: Input
|
|
1010
|
-
}],
|
|
1151
|
+
}], submitLabel: [{
|
|
1011
1152
|
type: Input
|
|
1012
|
-
}],
|
|
1153
|
+
}], attentionFields: [{
|
|
1013
1154
|
type: Input
|
|
1014
|
-
}],
|
|
1155
|
+
}], procedureOptions: [{
|
|
1156
|
+
type: Input
|
|
1157
|
+
}], modifierOptions: [{
|
|
1158
|
+
type: Input
|
|
1159
|
+
}], taxonomyOptions: [{
|
|
1160
|
+
type: Input
|
|
1161
|
+
}], submitter: [{
|
|
1162
|
+
type: Input
|
|
1163
|
+
}], submitted: [{
|
|
1164
|
+
type: Output
|
|
1165
|
+
}], submissionError: [{
|
|
1166
|
+
type: Output
|
|
1167
|
+
}], billChange: [{
|
|
1015
1168
|
type: Output
|
|
1169
|
+
}], onWindowDragOver: [{
|
|
1170
|
+
type: HostListener,
|
|
1171
|
+
args: ["window:dragover", ["$event"]]
|
|
1172
|
+
}], onWindowDragLeave: [{
|
|
1173
|
+
type: HostListener,
|
|
1174
|
+
args: ["window:dragleave", ["$event"]]
|
|
1175
|
+
}], onWindowDrop: [{
|
|
1176
|
+
type: HostListener,
|
|
1177
|
+
args: ["window:drop", ["$event"]]
|
|
1016
1178
|
}] } });
|
|
1017
1179
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/** Styled searchable dropdown shared by the submission form's procedure,
|
|
1054
|
-
* modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
|
|
1055
|
-
* package's ComboBox: query filtering, optional custom-code creation,
|
|
1056
|
-
* loading states, and infinite scroll via (endReached). */
|
|
1057
|
-
class MindBillComboBoxComponent {
|
|
1058
|
-
value = "";
|
|
1059
|
-
placeholder = "";
|
|
1060
|
-
options = [];
|
|
1061
|
-
disabled = false;
|
|
1062
|
-
loading = false;
|
|
1063
|
-
loadingMore = false;
|
|
1064
|
-
invalid = false;
|
|
1065
|
-
preserveValueOnOpen = false;
|
|
1066
|
-
ariaLabel = "";
|
|
1067
|
-
createOption;
|
|
1068
|
-
opened = new EventEmitter();
|
|
1069
|
-
queryChange = new EventEmitter();
|
|
1070
|
-
endReached = new EventEmitter();
|
|
1071
|
-
selected = new EventEmitter();
|
|
1072
|
-
open = false;
|
|
1073
|
-
query = "";
|
|
1074
|
-
closeTimer;
|
|
1075
|
-
get visible() {
|
|
1076
|
-
const q = this.query.trim().toLowerCase();
|
|
1077
|
-
const matches = q
|
|
1078
|
-
? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
|
|
1079
|
-
: this.options;
|
|
1080
|
-
const custom = this.createOption?.(this.query) ?? null;
|
|
1081
|
-
return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
|
|
1082
|
-
? [...matches, custom]
|
|
1083
|
-
: matches;
|
|
1084
|
-
}
|
|
1085
|
-
get showMenu() {
|
|
1086
|
-
return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
|
|
1087
|
-
}
|
|
1088
|
-
onFocus() {
|
|
1089
|
-
clearTimeout(this.closeTimer);
|
|
1090
|
-
const next = this.preserveValueOnOpen ? this.value : "";
|
|
1091
|
-
this.open = true;
|
|
1092
|
-
this.query = next;
|
|
1093
|
-
this.opened.emit();
|
|
1094
|
-
if (next)
|
|
1095
|
-
this.queryChange.emit(next);
|
|
1096
|
-
}
|
|
1097
|
-
onInput(event) {
|
|
1098
|
-
this.open = true;
|
|
1099
|
-
this.query = event.target.value;
|
|
1100
|
-
this.queryChange.emit(this.query);
|
|
1180
|
+
class MindBillBillLifecycleComponent {
|
|
1181
|
+
billId = "";
|
|
1182
|
+
sessionEndpoint = "/api/mindbill/session";
|
|
1183
|
+
apiBaseUrl = "https://app.mindbill.org";
|
|
1184
|
+
getSession;
|
|
1185
|
+
refreshInterval = 60_000;
|
|
1186
|
+
appearance = { preset: "mindbill" };
|
|
1187
|
+
billingError = new EventEmitter();
|
|
1188
|
+
store = new MindBillLifecycleStore();
|
|
1189
|
+
lifecycleStages = [
|
|
1190
|
+
{ id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
|
|
1191
|
+
{ id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
|
|
1192
|
+
{ id: "closed", label: "Closed" },
|
|
1193
|
+
];
|
|
1194
|
+
panel = "";
|
|
1195
|
+
notice = "";
|
|
1196
|
+
closeReason = "";
|
|
1197
|
+
payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
|
|
1198
|
+
review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
|
|
1199
|
+
correctionInitialBill = null;
|
|
1200
|
+
correctionAttachments = [];
|
|
1201
|
+
correctionAttentionFields = [];
|
|
1202
|
+
correctionReason = "";
|
|
1203
|
+
constructor() { effect(() => { const error = this.store.error(); if (error)
|
|
1204
|
+
this.billingError.emit(error); }); }
|
|
1205
|
+
ngOnChanges(changes) {
|
|
1206
|
+
if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
|
|
1207
|
+
if (!this.billId.trim()) {
|
|
1208
|
+
this.store.disconnect();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
|
|
1212
|
+
}
|
|
1101
1213
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1214
|
+
ngOnDestroy() { this.store.disconnect(); }
|
|
1215
|
+
get themeStyle() {
|
|
1216
|
+
return mindBillAngularAppearanceStyle(this.appearance);
|
|
1104
1217
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1218
|
+
stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
|
|
1219
|
+
lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "Submitted bill"; }
|
|
1220
|
+
currentStageIndex(data) { const value = data.lifecycle.state.toLowerCase(); if (value.includes("closed") || value.includes("written_off"))
|
|
1221
|
+
return 4; if (value.includes("second_review") || value.includes("ibr") || value.includes("lien") || value.includes("appeal"))
|
|
1222
|
+
return 3; if (value.includes("processed") || value.includes("paid") || value.includes("denied") || value.includes("rejected") || value.includes("response"))
|
|
1223
|
+
return 2; if (value.includes("accepted"))
|
|
1224
|
+
return 1; return 0; }
|
|
1225
|
+
activityLabel(type) { return type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
|
|
1226
|
+
historyExpanded = new Set();
|
|
1227
|
+
toggleHistory(id) { if (this.historyExpanded.has(id))
|
|
1228
|
+
this.historyExpanded.delete(id);
|
|
1229
|
+
else
|
|
1230
|
+
this.historyExpanded.add(id); }
|
|
1231
|
+
isHistoryOpen(id) { return this.historyExpanded.has(id); }
|
|
1232
|
+
historyDate(iso) { const parsed = new Date(iso); return Number.isNaN(parsed.getTime()) ? iso : parsed.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric" }); }
|
|
1233
|
+
payerContact(data) { const contacts = data.delivery.contacts; return contacts.adjusterName || contacts.adjusterPhone || contacts.adjusterEmail || contacts.claimsEmail || contacts.faxNumber || contacts.mailingAddress || "No payer contact details are available."; }
|
|
1234
|
+
beginAction(action, data) {
|
|
1235
|
+
if (action === "resubmit")
|
|
1236
|
+
this.prepareCorrection(data);
|
|
1237
|
+
else if (action === "post_payment")
|
|
1238
|
+
this.panel = "payment";
|
|
1239
|
+
else if (action === "second_review" || action === "independent_bill_review")
|
|
1240
|
+
this.panel = "review";
|
|
1241
|
+
else if (action === "close")
|
|
1242
|
+
this.panel = "close";
|
|
1243
|
+
else if (action === "view_eor" && data.eors[0])
|
|
1244
|
+
void this.openEor(data.eors[0].id, data.eors[0].filename);
|
|
1109
1245
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1246
|
+
async openAttachment(id, filename) { this.openBlob(await this.store.getAttachment(id), filename); }
|
|
1247
|
+
async openEor(id, filename) { this.openBlob(await this.store.getEor(id), filename); }
|
|
1248
|
+
async postPayment() { await this.store.postPayment(this.payment); this.panel = ""; this.notice = "Payment posted."; }
|
|
1249
|
+
async submitReview() { await this.store.submitSecondReview({ ...this.review, disputedAmount: this.review.disputedAmount > 0 ? this.review.disputedAmount : undefined, attachmentIds: this.store.data()?.bill.attachments.map((document) => document.id) ?? [], route: "ebill" }); this.panel = ""; this.notice = "Second Review submitted."; }
|
|
1250
|
+
async closeBill() { if (!this.closeReason.trim())
|
|
1251
|
+
return; await this.store.closeBill({ reason: this.closeReason }); this.panel = ""; this.notice = "Bill closed."; }
|
|
1252
|
+
resubmitFromForm = async (input) => {
|
|
1253
|
+
const data = await this.store.resubmitBill({
|
|
1254
|
+
bill: input.bill,
|
|
1255
|
+
...(input.documents?.length ? { documents: input.documents } : {}),
|
|
1256
|
+
...(this.correctionReason.trim() ? { reason: this.correctionReason.trim() } : {}),
|
|
1257
|
+
});
|
|
1258
|
+
return { billId: data.bill.id, bill: { id: data.bill.id, state: data.lifecycle.state } };
|
|
1259
|
+
};
|
|
1260
|
+
finishCorrection() { this.panel = ""; this.correctionReason = ""; this.notice = "Corrected submission sent."; }
|
|
1261
|
+
closeCorrectionBackdrop(event) { if (event.currentTarget === event.target)
|
|
1262
|
+
this.panel = ""; }
|
|
1263
|
+
prepareCorrection(data) {
|
|
1264
|
+
this.correctionInitialBill = correctionBill(data);
|
|
1265
|
+
this.correctionAttachments = data.bill.attachments.map((attachment) => ({
|
|
1266
|
+
filename: attachment.filename,
|
|
1267
|
+
documentType: correctionDocumentType(attachment.documentType),
|
|
1268
|
+
...(attachment.description ? { description: attachment.description } : {}),
|
|
1269
|
+
...(attachment.reportType ? { reportTypeCode: attachment.reportType } : {}),
|
|
1270
|
+
loadBlob: () => this.store.getAttachment(attachment.id),
|
|
1271
|
+
...(attachment.documentType === "w9" ? { locked: true, badge: "Auto-attached" } : {}),
|
|
1272
|
+
}));
|
|
1273
|
+
this.correctionAttentionFields = [...new Set(data.rejection?.issues?.flatMap((issue) => issue.fieldPaths || []) ?? [])];
|
|
1274
|
+
this.correctionReason = "";
|
|
1275
|
+
this.panel = "resubmit";
|
|
1114
1276
|
}
|
|
1115
|
-
|
|
1116
|
-
static
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
(
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1277
|
+
openBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noopener"; link.download = filename; link.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); }
|
|
1278
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1279
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillLifecycleComponent, isStandalone: true, selector: "mindbill-bill-lifecycle", inputs: { billId: "billId", sessionEndpoint: "sessionEndpoint", apiBaseUrl: "apiBaseUrl", getSession: "getSession", refreshInterval: "refreshInterval", appearance: "appearance" }, outputs: { billingError: "billingError" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1280
|
+
<section class="mb" [ngStyle]="themeStyle">
|
|
1281
|
+
@if (!billId) {
|
|
1282
|
+
<div class="state error"><strong>A submitted bill ID is required.</strong></div>
|
|
1283
|
+
} @else if (store.loading() && !store.data()) {
|
|
1284
|
+
<div class="state">Loading billing…</div>
|
|
1285
|
+
} @else if (store.error() && !store.data()) {
|
|
1286
|
+
<div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
|
|
1287
|
+
} @else if (store.data(); as data) {
|
|
1288
|
+
<header class="summary">
|
|
1289
|
+
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
1290
|
+
<div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
|
|
1291
|
+
</header>
|
|
1292
|
+
|
|
1293
|
+
@if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
|
|
1294
|
+
<mindbill-bill-rejection-notice
|
|
1295
|
+
class="rejection"
|
|
1296
|
+
[rejection]="data.rejection"
|
|
1297
|
+
[submittedAt]="data.lifecycle.submittedAt ?? null"
|
|
1298
|
+
[appearance]="appearance"
|
|
1299
|
+
/>
|
|
1300
|
+
} @else {
|
|
1301
|
+
<section class="card progress" aria-label="Bill lifecycle">
|
|
1302
|
+
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
1303
|
+
</section>
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
<section class="card snapshot" aria-label="Bill snapshot">
|
|
1307
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
1308
|
+
<dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
|
|
1309
|
+
</section>
|
|
1310
|
+
|
|
1311
|
+
<div class="columns">
|
|
1312
|
+
<section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty & interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
|
|
1313
|
+
<section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
|
|
1145
1314
|
</div>
|
|
1315
|
+
|
|
1316
|
+
<section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
|
|
1317
|
+
|
|
1318
|
+
<section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
|
|
1319
|
+
|
|
1320
|
+
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
|
|
1321
|
+
@if (data.history?.length) {
|
|
1322
|
+
<!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
|
|
1323
|
+
expandable rows (documents + compliance due dates), notes pale-yellow,
|
|
1324
|
+
277 responses expand into decoded status-code sentences. Rows arrive
|
|
1325
|
+
pre-worded from the MindBill API (BillLifecycleData.history). -->
|
|
1326
|
+
<div class="history">
|
|
1327
|
+
<div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
|
|
1328
|
+
<ul class="history-rows">
|
|
1329
|
+
@for (entry of data.history; track entry.id) {
|
|
1330
|
+
<li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
|
|
1331
|
+
<button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
|
|
1332
|
+
<span class="h-date">{{ historyDate(entry.date) }}</span>
|
|
1333
|
+
<span class="h-action">{{ entry.action }}</span>
|
|
1334
|
+
<span class="h-actor">{{ entry.actor || '—' }}</span>
|
|
1335
|
+
<span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
|
|
1336
|
+
</button>
|
|
1337
|
+
@if (entry.details; as details) {
|
|
1338
|
+
@if (isHistoryOpen(entry.id)) {
|
|
1339
|
+
<div class="history-details">
|
|
1340
|
+
@if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
|
|
1341
|
+
@if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
|
|
1342
|
+
@if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
|
|
1343
|
+
@if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
|
|
1344
|
+
@if (details.text) { <p class="h-text">{{ details.text }}</p> }
|
|
1345
|
+
</div>
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
</li>
|
|
1349
|
+
}
|
|
1350
|
+
</ul>
|
|
1351
|
+
</div>
|
|
1352
|
+
} @else {
|
|
1353
|
+
<ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
|
|
1354
|
+
}
|
|
1355
|
+
</section>
|
|
1356
|
+
|
|
1357
|
+
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
1358
|
+
<div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
|
|
1359
|
+
|
|
1360
|
+
@if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
|
|
1361
|
+
@if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
|
|
1362
|
+
@if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
|
|
1363
|
+
@if (panel === 'resubmit' && correctionInitialBill) {
|
|
1364
|
+
<div class="correction-backdrop" (mousedown)="closeCorrectionBackdrop($event)">
|
|
1365
|
+
<section class="correction-dialog" role="dialog" aria-modal="true" aria-label="Correct and resubmit">
|
|
1366
|
+
<button type="button" class="dialog-close" aria-label="Close" (click)="panel=''">×</button>
|
|
1367
|
+
<header><h2>Correct and resubmit</h2><p>The original submission remains immutable. Review the rejected values and create the next submission attempt on this bill.</p></header>
|
|
1368
|
+
@if (data.rejection; as rejection) {
|
|
1369
|
+
<div class="correction-reason"><strong>{{ rejection.reason || 'Why it was rejected' }}</strong>@for (issue of rejection.issues || []; track issue.code) { <span><b>{{ issue.code }}</b> {{ issue.description }}</span> }</div>
|
|
1370
|
+
}
|
|
1371
|
+
@if (payerContact(data) !== 'No payer contact details are available.') { <p class="contact-hint"><strong>Questions?</strong> {{ payerContact(data) }}</p> }
|
|
1372
|
+
<label class="correction-note">Correction note (optional)<textarea [(ngModel)]="correctionReason" name="correctionReason" placeholder="What changed before resubmission?"></textarea></label>
|
|
1373
|
+
<mindbill-bill-submission
|
|
1374
|
+
[initialBill]="correctionInitialBill"
|
|
1375
|
+
[attachments]="correctionAttachments"
|
|
1376
|
+
[attentionFields]="correctionAttentionFields"
|
|
1377
|
+
[appearance]="appearance"
|
|
1378
|
+
[sessionEndpoint]="sessionEndpoint"
|
|
1379
|
+
[getSession]="getSession"
|
|
1380
|
+
[apiBaseUrl]="apiBaseUrl"
|
|
1381
|
+
[submitter]="resubmitFromForm"
|
|
1382
|
+
heading="Corrected bill information"
|
|
1383
|
+
description="Correct the highlighted values, confirm every required field, then resubmit."
|
|
1384
|
+
submitLabel="Resubmit bill"
|
|
1385
|
+
(submitted)="finishCorrection()"
|
|
1386
|
+
/>
|
|
1387
|
+
</section>
|
|
1388
|
+
</div>
|
|
1389
|
+
}
|
|
1146
1390
|
}
|
|
1147
|
-
</
|
|
1148
|
-
`, isInline: true, styles: [":host{display:block}.
|
|
1391
|
+
</section>
|
|
1392
|
+
`, isInline: true, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}.correction-backdrop{position:fixed;inset:0;z-index:100;overflow:auto;background:#172b37b8;padding:28px}.correction-dialog{position:relative;max-width:1180px;margin:0 auto;background:var(--bg);border-radius:var(--r);box-shadow:0 24px 80px #0006;padding:26px}.correction-dialog>header{padding-right:48px;margin-bottom:18px}.correction-dialog h2{margin:0 0 6px}.correction-dialog p{margin:0;color:var(--m)}.dialog-close{position:absolute;right:20px;top:20px;width:40px;height:40px;padding:0;font-size:24px}.correction-reason{display:grid;gap:8px;margin-bottom:14px;border-left:4px solid #c43d3d;border-radius:8px;background:#fff5f5;padding:14px;color:#8f2525}.correction-reason span{display:block}.correction-reason b{margin-right:6px}.contact-hint{margin:0 0 14px!important;border:1px solid var(--b);border-radius:8px;background:var(--s);padding:12px!important;color:var(--t)!important}.correction-note{display:grid;gap:6px;margin-bottom:18px;font-weight:700}.correction-note textarea{min-height:76px}.correction-dialog mindbill-bill-submission{display:block}.correction-dialog .mb{padding:0}@media(max-width:760px){.correction-backdrop{padding:0}.correction-dialog{min-height:100%;border-radius:0;padding:18px 12px}.dialog-close{right:12px;top:12px}}\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.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MindBillBillRejectionNoticeComponent, selector: "mindbill-bill-rejection-notice", inputs: ["rejection", "submittedAt", "title", "appearance"] }, { kind: "component", type: MindBillBillSubmissionComponent, selector: "mindbill-bill-submission", inputs: ["initialBill", "attachments", "appearance", "sessionEndpoint", "getSession", "apiBaseUrl", "heading", "description", "submitLabel", "attentionFields", "procedureOptions", "modifierOptions", "taxonomyOptions", "submitter"], outputs: ["submitted", "submissionError", "billChange"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
|
|
1149
1393
|
}
|
|
1150
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
1394
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
|
|
1151
1395
|
type: Component,
|
|
1152
|
-
args: [{ selector: "mindbill-
|
|
1153
|
-
<
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
(
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1396
|
+
args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule, MindBillBillRejectionNoticeComponent, MindBillBillSubmissionComponent], template: `
|
|
1397
|
+
<section class="mb" [ngStyle]="themeStyle">
|
|
1398
|
+
@if (!billId) {
|
|
1399
|
+
<div class="state error"><strong>A submitted bill ID is required.</strong></div>
|
|
1400
|
+
} @else if (store.loading() && !store.data()) {
|
|
1401
|
+
<div class="state">Loading billing…</div>
|
|
1402
|
+
} @else if (store.error() && !store.data()) {
|
|
1403
|
+
<div class="state error"><strong>Billing is temporarily unavailable</strong><span>{{ store.error()?.message }}</span><button type="button" (click)="store.refresh()">Try again</button></div>
|
|
1404
|
+
} @else if (store.data(); as data) {
|
|
1405
|
+
<header class="summary">
|
|
1406
|
+
<div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
|
|
1407
|
+
<div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
|
|
1408
|
+
</header>
|
|
1409
|
+
|
|
1410
|
+
@if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
|
|
1411
|
+
<mindbill-bill-rejection-notice
|
|
1412
|
+
class="rejection"
|
|
1413
|
+
[rejection]="data.rejection"
|
|
1414
|
+
[submittedAt]="data.lifecycle.submittedAt ?? null"
|
|
1415
|
+
[appearance]="appearance"
|
|
1416
|
+
/>
|
|
1417
|
+
} @else {
|
|
1418
|
+
<section class="card progress" aria-label="Bill lifecycle">
|
|
1419
|
+
<ol>@for (stage of lifecycleStages; track stage.id; let i=$index) { <li [class.complete]="i < currentStageIndex(data)" [class.current]="i === currentStageIndex(data)"><b>{{ i < currentStageIndex(data) ? '✓' : i + 1 }}</b><span>{{ stage.label }}</span></li> }</ol>
|
|
1420
|
+
</section>
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
<section class="card snapshot" aria-label="Bill snapshot">
|
|
1424
|
+
<div class="card-title"><div><h3>Bill snapshot</h3><p>The immutable values submitted to the payer.</p></div></div>
|
|
1425
|
+
<dl><div><dt>Patient</dt><dd>{{ data.patient.name }}</dd></div><div><dt>Claim</dt><dd>{{ data.injury.claimNumber || '—' }}</dd></div><div><dt>Payer</dt><dd>{{ data.delivery.payerName || data.injury.claimsAdminName || '—' }}</dd></div><div><dt>Date of service</dt><dd>{{ data.bill.dos || '—' }}</dd></div><div><dt>Charged</dt><dd>{{ data.bill.totalCharge | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.bill.balanceDue | currency }}</dd></div></dl>
|
|
1426
|
+
</section>
|
|
1427
|
+
|
|
1428
|
+
<div class="columns">
|
|
1429
|
+
<section class="card"><div class="card-title"><div><h3>Remittance</h3><p>Amounts reported by the payer and posted to this bill.</p></div></div><dl><div><dt>Billed</dt><dd>{{ data.remittance.billedAmount | currency }}</dd></div><div><dt>Payer reported</dt><dd>{{ data.remittance.payerReportedPaid == null ? '—' : (data.remittance.payerReportedPaid | currency) }}</dd></div><div><dt>Principal posted</dt><dd>{{ data.remittance.postedPrincipal | currency }}</dd></div><div><dt>Penalty & interest</dt><dd>{{ data.remittance.postedAdditional | currency }}</dd></div><div><dt>Total received</dt><dd>{{ data.remittance.totalPostedCash | currency }}</dd></div><div><dt>Balance</dt><dd>{{ data.remittance.balanceDue | currency }}</dd></div></dl></section>
|
|
1430
|
+
<section class="card"><div class="card-title"><div><h3>{{ data.delivery.payerName || 'Payer' }}</h3><p>Billing and follow-up contacts.</p></div></div><p class="muted">{{ payerContact(data) }}</p></section>
|
|
1181
1431
|
</div>
|
|
1432
|
+
|
|
1433
|
+
<section class="card"><div class="card-title"><div><h3>Documents</h3><p>Documents preserved with the submitted bill.</p></div><span>{{ data.bill.attachments.length }}</span></div>@if (data.bill.attachments.length) { <ul class="rows">@for (doc of data.bill.attachments; track doc.id) { <li><div><strong>{{ doc.filename }}</strong><small>{{ doc.description || doc.documentType }}</small></div><button type="button" (click)="openAttachment(doc.id, doc.filename)">View</button></li> }</ul> } @else { <p class="muted">No documents available.</p> }</section>
|
|
1434
|
+
|
|
1435
|
+
<section class="card"><div class="card-title"><div><h3>Payments</h3><p>Payments posted to this bill.</p></div><span>{{ data.payments.length }}</span></div>@if (data.payments.length) { <ul class="rows">@for (payment of data.payments; track payment.id) { <li><div><strong>{{ payment.amount | currency }}</strong><small>{{ payment.method | uppercase }} · {{ payment.depositDate || payment.postedAt }}</small></div></li> }</ul> } @else { <p class="muted">No payments posted.</p> }</section>
|
|
1436
|
+
|
|
1437
|
+
<section class="card"><div class="card-title"><div><h3>Bill history</h3><p>Submissions, payer responses, follow-up, and payments.</p></div><span>{{ data.history?.length ?? data.activity.length }}</span></div>
|
|
1438
|
+
@if (data.history?.length) {
|
|
1439
|
+
<!-- daisyBill-style Date/Action/User/Details table: submissions are pale-blue
|
|
1440
|
+
expandable rows (documents + compliance due dates), notes pale-yellow,
|
|
1441
|
+
277 responses expand into decoded status-code sentences. Rows arrive
|
|
1442
|
+
pre-worded from the MindBill API (BillLifecycleData.history). -->
|
|
1443
|
+
<div class="history">
|
|
1444
|
+
<div class="history-head" aria-hidden="true"><span>Date</span><span>Action</span><span>User</span><span>Details</span></div>
|
|
1445
|
+
<ul class="history-rows">
|
|
1446
|
+
@for (entry of data.history; track entry.id) {
|
|
1447
|
+
<li [class.is-submission]="entry.tone === 'submission'" [class.is-note]="entry.tone === 'note'" [class.is-problem]="entry.tone === 'problem'">
|
|
1448
|
+
<button type="button" class="history-line" [disabled]="!entry.details" [attr.aria-expanded]="entry.details ? isHistoryOpen(entry.id) : null" (click)="toggleHistory(entry.id)">
|
|
1449
|
+
<span class="h-date">{{ historyDate(entry.date) }}</span>
|
|
1450
|
+
<span class="h-action">{{ entry.action }}</span>
|
|
1451
|
+
<span class="h-actor">{{ entry.actor || '—' }}</span>
|
|
1452
|
+
<span class="h-summary">@if (entry.details) { <i [class.open]="isHistoryOpen(entry.id)" aria-hidden="true"></i> }{{ entry.summary }}</span>
|
|
1453
|
+
</button>
|
|
1454
|
+
@if (entry.details; as details) {
|
|
1455
|
+
@if (isHistoryOpen(entry.id)) {
|
|
1456
|
+
<div class="history-details">
|
|
1457
|
+
@if (details.rows?.length) { <dl class="h-kv">@for (row of details.rows ?? []; track row.label) { <div><dt>{{ row.label }}</dt><dd>{{ row.value }}</dd></div> }</dl> }
|
|
1458
|
+
@if (details.documents?.length) { <dl class="h-kv"><div><dt>Documents</dt><dd>@for (doc of details.documents ?? []; track doc.id) { <span class="h-doc">{{ doc.filename }}</span> }</dd></div></dl> }
|
|
1459
|
+
@if (details.codes?.length) { <dl class="h-codes">@for (code of details.codes ?? []; track code.code) { <div><dt>{{ code.code }}</dt><dd>{{ code.text }}</dd></div> }</dl> }
|
|
1460
|
+
@if (details.complianceDueDates?.length) { <div class="h-due"><em>Compliance Due Dates</em><dl>@for (due of details.complianceDueDates ?? []; track due.text) { <div><dt>{{ historyDate(due.date) }}</dt><dd>{{ due.text }}</dd></div> }</dl></div> }
|
|
1461
|
+
@if (details.text) { <p class="h-text">{{ details.text }}</p> }
|
|
1462
|
+
</div>
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
</li>
|
|
1466
|
+
}
|
|
1467
|
+
</ul>
|
|
1468
|
+
</div>
|
|
1469
|
+
} @else {
|
|
1470
|
+
<ul class="timeline">@for (event of data.activity; track event.id) { <li><i></i><div><strong>{{ activityLabel(event.type) }}</strong><p>{{ event.description }}</p><small>{{ event.createdAt | date:'medium' }}</small></div></li> }</ul>
|
|
1471
|
+
}
|
|
1472
|
+
</section>
|
|
1473
|
+
|
|
1474
|
+
@if (notice) { <p class="notice">{{ notice }}</p> }
|
|
1475
|
+
<div class="actions">@for (action of data.lifecycle.actions; track action.id) { @if (action.id !== 'view_eor' || data.eors.length) { <button type="button" [class.primary]="action.primary" [disabled]="!action.enabled || store.mutating()" (click)="beginAction(action.id, data)">{{ action.label }}</button> } }</div>
|
|
1476
|
+
|
|
1477
|
+
@if (panel === 'payment') { <form class="card action-form" (submit)="$event.preventDefault(); postPayment()"><h3>Post payment</h3><label>Amount<input required type="number" min="0.01" step="0.01" [(ngModel)]="payment.amount" name="amount"></label><label>Method<select [(ngModel)]="payment.method" name="method"><option value="check">Check</option><option value="eft">EFT</option></select></label><label>Deposit date<input required type="date" [(ngModel)]="payment.depositDate" name="depositDate"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Post payment</button></div></form> }
|
|
1478
|
+
@if (panel === 'review') { <form class="card action-form" (submit)="$event.preventDefault(); submitReview()"><h3>Submit Second Bill Review</h3><label>Reason<textarea required [(ngModel)]="review.reason" name="reason"></textarea></label><label>Payer claim control number<input required [(ngModel)]="review.payerClaimControlNumber" name="control"></label><label>Disputed amount<input type="number" min="0" step="0.01" [(ngModel)]="review.disputedAmount" name="disputed"></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Submit review</button></div></form> }
|
|
1479
|
+
@if (panel === 'close') { <form class="card action-form" (submit)="$event.preventDefault(); closeBill()"><h3>Close bill</h3><label>Reason<textarea required [(ngModel)]="closeReason" name="closeReason"></textarea></label><div><button type="button" (click)="panel=''">Cancel</button><button class="primary" type="submit">Close bill</button></div></form> }
|
|
1480
|
+
@if (panel === 'resubmit' && correctionInitialBill) {
|
|
1481
|
+
<div class="correction-backdrop" (mousedown)="closeCorrectionBackdrop($event)">
|
|
1482
|
+
<section class="correction-dialog" role="dialog" aria-modal="true" aria-label="Correct and resubmit">
|
|
1483
|
+
<button type="button" class="dialog-close" aria-label="Close" (click)="panel=''">×</button>
|
|
1484
|
+
<header><h2>Correct and resubmit</h2><p>The original submission remains immutable. Review the rejected values and create the next submission attempt on this bill.</p></header>
|
|
1485
|
+
@if (data.rejection; as rejection) {
|
|
1486
|
+
<div class="correction-reason"><strong>{{ rejection.reason || 'Why it was rejected' }}</strong>@for (issue of rejection.issues || []; track issue.code) { <span><b>{{ issue.code }}</b> {{ issue.description }}</span> }</div>
|
|
1487
|
+
}
|
|
1488
|
+
@if (payerContact(data) !== 'No payer contact details are available.') { <p class="contact-hint"><strong>Questions?</strong> {{ payerContact(data) }}</p> }
|
|
1489
|
+
<label class="correction-note">Correction note (optional)<textarea [(ngModel)]="correctionReason" name="correctionReason" placeholder="What changed before resubmission?"></textarea></label>
|
|
1490
|
+
<mindbill-bill-submission
|
|
1491
|
+
[initialBill]="correctionInitialBill"
|
|
1492
|
+
[attachments]="correctionAttachments"
|
|
1493
|
+
[attentionFields]="correctionAttentionFields"
|
|
1494
|
+
[appearance]="appearance"
|
|
1495
|
+
[sessionEndpoint]="sessionEndpoint"
|
|
1496
|
+
[getSession]="getSession"
|
|
1497
|
+
[apiBaseUrl]="apiBaseUrl"
|
|
1498
|
+
[submitter]="resubmitFromForm"
|
|
1499
|
+
heading="Corrected bill information"
|
|
1500
|
+
description="Correct the highlighted values, confirm every required field, then resubmit."
|
|
1501
|
+
submitLabel="Resubmit bill"
|
|
1502
|
+
(submitted)="finishCorrection()"
|
|
1503
|
+
/>
|
|
1504
|
+
</section>
|
|
1505
|
+
</div>
|
|
1506
|
+
}
|
|
1182
1507
|
}
|
|
1183
|
-
</
|
|
1184
|
-
`, styles: [":host{display:block}.
|
|
1185
|
-
}], propDecorators: {
|
|
1186
|
-
type: Input
|
|
1187
|
-
}], placeholder: [{
|
|
1508
|
+
</section>
|
|
1509
|
+
`, styles: [":host{display:block}.mb{font-family:var(--font);color:var(--t);background:var(--bg);padding:20px}.summary,.card,.columns,.rejection{display:block;max-width:1120px;margin:0 auto 16px}.summary{display:flex;justify-content:space-between;align-items:end}.summary h2{margin:4px 0}.summary p,.muted{color:var(--m)}.eyebrow,dt{font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--m)}.money{text-align:right}.money span{display:block;font-size:28px;font-weight:800}.money small{color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);padding:20px}.card-title{display:flex;justify-content:space-between;gap:20px}.card-title h3{margin:0}.card-title p{margin:5px 0;color:var(--m)}.progress ol{display:flex;list-style:none;padding:0;margin:0}.progress li{flex:1;text-align:center;position:relative;color:var(--m)}.progress li:before{content:\"\";position:absolute;top:16px;left:0;right:0;border-top:2px solid var(--b)}.progress li:first-child:before{left:50%}.progress li:last-child:before{right:50%}.progress b{position:relative;z-index:1;display:grid;place-items:center;width:32px;height:32px;margin:auto;border:2px solid var(--b);border-radius:50%;background:var(--s)}.progress .complete b,.progress .current b{border-color:var(--a);background:var(--a);color:var(--ac)}.progress span{display:block;margin-top:8px;font-size:12px;font-weight:700}.snapshot dl,.columns dl{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.snapshot dd,.columns dd{font-weight:700;margin:6px 0}.columns{display:grid;grid-template-columns:1fr 1fr;gap:16px}.columns .card{margin:0}.rows,.timeline{list-style:none;margin:12px 0 0;padding:0}.rows li{display:flex;justify-content:space-between;align-items:center;padding:12px 0;border-top:1px solid var(--b)}.rows small{display:block;color:var(--m);margin-top:3px}.timeline li{display:flex;gap:14px;padding:10px 0}.timeline i{width:10px;height:10px;margin-top:5px;border:3px solid var(--a);border-radius:50%}.timeline p{margin:4px 0}.timeline small{color:var(--m)}button,input,select,textarea{font:inherit;border:1px solid var(--b);border-radius:var(--cr);padding:9px 12px;background:var(--s);color:var(--t)}button{cursor:pointer;font-weight:700}.primary{background:var(--a);border-color:var(--a);color:var(--ac)}.actions{max-width:1120px;margin:0 auto;display:flex;justify-content:flex-end;gap:10px}.action-form{display:grid;gap:12px}.action-form label{display:grid;gap:5px;font-weight:700}.action-form div{display:flex;justify-content:flex-end;gap:10px}.state{max-width:720px;margin:auto;padding:32px;text-align:center}.state span{display:block;margin:8px}.error{color:#9b1c1c}.notice{max-width:1120px;margin:0 auto 12px;color:var(--m)}@media(max-width:760px){.mb{padding:12px}.summary,.columns{display:block}.money{text-align:left;margin-top:12px}.columns .card{margin-bottom:12px}.snapshot dl,.columns dl{grid-template-columns:1fr 1fr}.progress span{font-size:10px}.card{padding:15px}}.history{border:1px solid var(--b);border-radius:10px;overflow:hidden;margin-top:12px}.history-head{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;padding:8px 14px;background:color-mix(in srgb,var(--b) 26%,var(--s));color:var(--m);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.history-rows{list-style:none;margin:0;padding:0}.history-rows>li{border-top:1px solid var(--b)}.history-rows>li.is-submission{background:color-mix(in srgb,var(--a) 9%,var(--s))}.history-rows>li.is-note{background:color-mix(in srgb,#f2c344 13%,var(--s))}.history-line{display:grid;grid-template-columns:96px 150px 130px minmax(0,1fr);gap:12px;width:100%;padding:10px 14px;border:0;border-radius:0;background:transparent;text-align:left;font-weight:400;cursor:default}.history-line:not(:disabled){cursor:pointer}.history-line:not(:disabled):hover .h-summary{text-decoration:underline}.h-date,.h-actor{color:var(--m);font-size:13px;overflow-wrap:anywhere}.h-action{font-weight:700;font-size:13.5px}.is-problem .h-action{color:#9b1c1c}.h-summary{display:flex;gap:7px;min-width:0;font-size:13.5px;line-height:1.45;overflow-wrap:anywhere}.h-summary i{flex:0 0 auto;margin-top:5px;width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--m);border-radius:0;transition:transform .12s ease}.h-summary i.open{transform:rotate(90deg)}.history-details{padding:4px 14px 14px 122px;font-size:13.5px;line-height:1.5}.h-kv,.h-codes,.h-due dl{display:grid;gap:4px;margin:0}.h-kv>div{display:grid;grid-template-columns:180px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-kv dt,.h-codes dt,.h-due dt{font-size:13px;font-weight:700;letter-spacing:0;text-transform:none;color:var(--t)}.h-kv dd,.h-codes dd,.h-due dd{margin:0;overflow-wrap:anywhere}.h-doc{display:block}.h-codes>div{display:grid;grid-template-columns:56px minmax(0,1fr);gap:12px}.h-codes dt{font-weight:800}.h-due{margin-top:8px}.h-due em{display:block;color:var(--m);font-size:12.5px;margin-bottom:4px}.h-due dl>div{display:grid;grid-template-columns:96px minmax(0,1fr);gap:12px;border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);padding-top:4px}.h-text{margin:8px 0 0;white-space:pre-wrap}@media(max-width:760px){.history-head{display:none}.history-line{grid-template-columns:1fr;gap:2px;padding:10px 12px}.history-details{padding:4px 12px 12px}.h-kv>div,.h-due dl>div{grid-template-columns:1fr}}.correction-backdrop{position:fixed;inset:0;z-index:100;overflow:auto;background:#172b37b8;padding:28px}.correction-dialog{position:relative;max-width:1180px;margin:0 auto;background:var(--bg);border-radius:var(--r);box-shadow:0 24px 80px #0006;padding:26px}.correction-dialog>header{padding-right:48px;margin-bottom:18px}.correction-dialog h2{margin:0 0 6px}.correction-dialog p{margin:0;color:var(--m)}.dialog-close{position:absolute;right:20px;top:20px;width:40px;height:40px;padding:0;font-size:24px}.correction-reason{display:grid;gap:8px;margin-bottom:14px;border-left:4px solid #c43d3d;border-radius:8px;background:#fff5f5;padding:14px;color:#8f2525}.correction-reason span{display:block}.correction-reason b{margin-right:6px}.contact-hint{margin:0 0 14px!important;border:1px solid var(--b);border-radius:8px;background:var(--s);padding:12px!important;color:var(--t)!important}.correction-note{display:grid;gap:6px;margin-bottom:18px;font-weight:700}.correction-note textarea{min-height:76px}.correction-dialog mindbill-bill-submission{display:block}.correction-dialog .mb{padding:0}@media(max-width:760px){.correction-backdrop{padding:0}.correction-dialog{min-height:100%;border-radius:0;padding:18px 12px}.dialog-close{right:12px;top:12px}}\n"] }]
|
|
1510
|
+
}], ctorParameters: () => [], propDecorators: { billId: [{
|
|
1188
1511
|
type: Input
|
|
1189
|
-
}],
|
|
1512
|
+
}], sessionEndpoint: [{
|
|
1190
1513
|
type: Input
|
|
1191
|
-
}],
|
|
1514
|
+
}], apiBaseUrl: [{
|
|
1192
1515
|
type: Input
|
|
1193
|
-
}],
|
|
1516
|
+
}], getSession: [{
|
|
1194
1517
|
type: Input
|
|
1195
|
-
}],
|
|
1518
|
+
}], refreshInterval: [{
|
|
1196
1519
|
type: Input
|
|
1197
|
-
}],
|
|
1520
|
+
}], appearance: [{
|
|
1198
1521
|
type: Input
|
|
1199
|
-
}],
|
|
1522
|
+
}], billingError: [{
|
|
1523
|
+
type: Output
|
|
1524
|
+
}] } });
|
|
1525
|
+
const CORRECTION_DOCUMENT_TYPES = new Set(["final_report", "letter_of_attestation", "proof_of_service", "form_122", "return_to_work_voucher", "w9", "medical_records", "appeal", "other"]);
|
|
1526
|
+
function correctionDocumentType(value) { return (CORRECTION_DOCUMENT_TYPES.has(value) ? value : "other"); }
|
|
1527
|
+
function correctionBill(data) {
|
|
1528
|
+
const billing = data.bill.billingSnapshot?.billingProvider;
|
|
1529
|
+
const rendering = data.bill.billingSnapshot?.renderingProvider;
|
|
1530
|
+
const location = data.bill.billingSnapshot?.placeOfService;
|
|
1531
|
+
const nameParts = data.patient.name.trim().split(/\s+/);
|
|
1532
|
+
return {
|
|
1533
|
+
billingMode: data.bill.billingMode,
|
|
1534
|
+
patient: {
|
|
1535
|
+
firstName: data.patient.firstName || nameParts[0] || "",
|
|
1536
|
+
...(data.patient.middleName ? { middleName: data.patient.middleName } : {}),
|
|
1537
|
+
lastName: data.patient.lastName || nameParts.slice(1).join(" ") || "",
|
|
1538
|
+
dateOfBirth: data.patient.dob || "",
|
|
1539
|
+
...(data.patient.phone ? { phone: data.patient.phone } : {}),
|
|
1540
|
+
address: { line1: data.patient.address?.line1 || "", city: data.patient.address?.city || "", state: data.patient.address?.state || "", postalCode: data.patient.address?.postalCode || "" },
|
|
1541
|
+
},
|
|
1542
|
+
claim: {
|
|
1543
|
+
claimNumber: data.injury.claimNumber || "",
|
|
1544
|
+
...(data.injury.adjNumber ? { adjNumber: data.injury.adjNumber } : {}),
|
|
1545
|
+
employer: data.injury.employer || "",
|
|
1546
|
+
dateOfInjury: data.injury.doi || "",
|
|
1547
|
+
...(data.injury.injuryDescription ? { description: data.injury.injuryDescription } : {}),
|
|
1548
|
+
claimsAdministrator: { id: data.injury.claimsAdminId || "", name: data.injury.claimsAdminName || "" },
|
|
1549
|
+
},
|
|
1550
|
+
service: { date: data.bill.dos, ...(data.bill.dosEnd !== undefined ? { endDate: data.bill.dosEnd } : {}), ...(data.bill.authorizationNumber !== undefined ? { authorizationNumber: data.bill.authorizationNumber } : {}) },
|
|
1551
|
+
billingProvider: {
|
|
1552
|
+
name: billing?.name || "", taxId: billing?.taxId || "", npi: billing?.npi || "", phone: billing?.phone || "",
|
|
1553
|
+
address: { line1: billing?.billingStreet || "", city: billing?.billingCity || "", state: billing?.billingState || "", postalCode: billing?.billingZip || "" },
|
|
1554
|
+
},
|
|
1555
|
+
renderingProvider: {
|
|
1556
|
+
name: rendering?.name || "", npi: rendering?.npi || "", taxonomy: rendering?.taxonomy || "",
|
|
1557
|
+
...(rendering?.specialty ? { specialty: rendering.specialty } : {}), ...(rendering?.licenseNumber ? { licenseNumber: rendering.licenseNumber } : {}), ...(rendering?.licenseState ? { licenseState: rendering.licenseState } : {}),
|
|
1558
|
+
...(rendering?.isQME !== undefined ? { isQme: rendering.isQME } : {}), ...(rendering?.isAME !== undefined ? { isAme: rendering.isAME } : {}),
|
|
1559
|
+
},
|
|
1560
|
+
serviceLocation: {
|
|
1561
|
+
...(location?.name ? { name: location.name } : {}),
|
|
1562
|
+
address: { line1: location?.street || "", city: location?.city || "", state: location?.state || "", postalCode: location?.zip || "" },
|
|
1563
|
+
placeOfServiceCode: location?.posCode || "",
|
|
1564
|
+
},
|
|
1565
|
+
diagnoses: [...(data.injury.diagnosisCodes || [])],
|
|
1566
|
+
serviceLines: data.bill.lineItems.map((line) => ({ code: line.code, modifiers: [...line.modifiers], units: line.units, charge: line.charge, ...(line.serviceDate ? { serviceDate: line.serviceDate } : {}), ...(line.serviceDateEnd !== undefined ? { serviceDateEnd: line.serviceDateEnd } : {}), ...(line.diagnosisPointers ? { diagnosisPointers: [...line.diagnosisPointers] } : {}) })),
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const EMPTY_PROCEDURE_LINE = {
|
|
1571
|
+
code: "",
|
|
1572
|
+
modifiers: [],
|
|
1573
|
+
units: 1,
|
|
1574
|
+
charge: 0,
|
|
1575
|
+
};
|
|
1576
|
+
function isEmptyProcedureLine(line) {
|
|
1577
|
+
return !line.code.trim()
|
|
1578
|
+
&& line.modifiers.length === 0
|
|
1579
|
+
&& line.units === 1
|
|
1580
|
+
&& !line.serviceDate
|
|
1581
|
+
&& !line.serviceDateEnd
|
|
1582
|
+
&& !(line.diagnosisPointers?.length)
|
|
1583
|
+
&& !line.charge;
|
|
1584
|
+
}
|
|
1585
|
+
/** Keeps completed/partial lines plus one keyboard-ready empty row. */
|
|
1586
|
+
function ensureTrailingProcedureLine(lines) {
|
|
1587
|
+
const entered = lines
|
|
1588
|
+
.filter((line) => !isEmptyProcedureLine(line))
|
|
1589
|
+
.map((line) => ({ ...line, modifiers: [...line.modifiers] }));
|
|
1590
|
+
return entered.length < 50
|
|
1591
|
+
? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
|
|
1592
|
+
: entered;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const THEME$3 = {
|
|
1596
|
+
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" },
|
|
1597
|
+
"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" },
|
|
1598
|
+
"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" },
|
|
1599
|
+
"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" },
|
|
1600
|
+
};
|
|
1601
|
+
function mindBillAgingDays(bill, now = new Date()) {
|
|
1602
|
+
if (bill.agingDays != null)
|
|
1603
|
+
return Math.max(0, Math.floor(bill.agingDays));
|
|
1604
|
+
const source = bill.submittedAt ?? bill.updatedAt;
|
|
1605
|
+
if (!source)
|
|
1606
|
+
return 0;
|
|
1607
|
+
const time = new Date(source).getTime();
|
|
1608
|
+
return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
|
|
1609
|
+
}
|
|
1610
|
+
function mindBillAgingBucket(days) {
|
|
1611
|
+
if (days <= 30)
|
|
1612
|
+
return "current";
|
|
1613
|
+
if (days <= 60)
|
|
1614
|
+
return "31-60";
|
|
1615
|
+
if (days <= 90)
|
|
1616
|
+
return "61-90";
|
|
1617
|
+
return "91+";
|
|
1618
|
+
}
|
|
1619
|
+
function summarizeMindBillDashboard(bills, now = new Date()) {
|
|
1620
|
+
const aging = [
|
|
1621
|
+
{ id: "current", label: "0–30 days", count: 0, balance: 0 },
|
|
1622
|
+
{ id: "31-60", label: "31–60 days", count: 0, balance: 0 },
|
|
1623
|
+
{ id: "61-90", label: "61–90 days", count: 0, balance: 0 },
|
|
1624
|
+
{ id: "91+", label: "91+ days", count: 0, balance: 0 },
|
|
1625
|
+
];
|
|
1626
|
+
for (const bill of bills) {
|
|
1627
|
+
const bucket = aging.find((item) => item.id === mindBillAgingBucket(mindBillAgingDays(bill, now)));
|
|
1628
|
+
bucket.count += 1;
|
|
1629
|
+
bucket.balance += bill.balanceDue;
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
totalBilled: bills.reduce((sum, item) => sum + item.totalCharge, 0),
|
|
1633
|
+
totalPaid: bills.reduce((sum, item) => sum + item.totalPaid, 0),
|
|
1634
|
+
outstanding: bills.reduce((sum, item) => sum + item.balanceDue, 0),
|
|
1635
|
+
openCount: bills.filter((item) => !/closed|paid|written.off/i.test(item.state)).length,
|
|
1636
|
+
submittedThisMonth: bills.filter((item) => sameMonth(item.submittedAt, now)).length,
|
|
1637
|
+
closedThisMonth: bills.filter((item) => /closed|paid|written.off/i.test(item.state) && sameMonth(item.updatedAt ?? item.submittedAt, now)).length,
|
|
1638
|
+
bills,
|
|
1639
|
+
aging,
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
function sameMonth(value, now) {
|
|
1643
|
+
if (!value)
|
|
1644
|
+
return false;
|
|
1645
|
+
const date = new Date(value);
|
|
1646
|
+
return Number.isFinite(date.getTime()) && date.getUTCFullYear() === now.getUTCFullYear() && date.getUTCMonth() === now.getUTCMonth();
|
|
1647
|
+
}
|
|
1648
|
+
function buildMindBillReportRows(bills, dimension) {
|
|
1649
|
+
const rows = new Map();
|
|
1650
|
+
for (const bill of bills) {
|
|
1651
|
+
const key = dimension === "payer" ? bill.payerName || "Unassigned payer" : dimension === "aging" ? mindBillAgingBucket(mindBillAgingDays(bill)) : bill.state;
|
|
1652
|
+
const label = key.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase());
|
|
1653
|
+
const row = rows.get(key) ?? { key, label, billCount: 0, totalBilled: 0, totalPaid: 0, balanceDue: 0 };
|
|
1654
|
+
row.billCount += 1;
|
|
1655
|
+
row.totalBilled += bill.totalCharge;
|
|
1656
|
+
row.totalPaid += bill.totalPaid;
|
|
1657
|
+
row.balanceDue += bill.balanceDue;
|
|
1658
|
+
rows.set(key, row);
|
|
1659
|
+
}
|
|
1660
|
+
return [...rows.values()].sort((a, b) => b.balanceDue - a.balanceDue || a.label.localeCompare(b.label));
|
|
1661
|
+
}
|
|
1662
|
+
function buildMindBillReportCsv(rows) {
|
|
1663
|
+
const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
1664
|
+
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");
|
|
1665
|
+
}
|
|
1666
|
+
class DashboardBase {
|
|
1667
|
+
appearance = { preset: "mindbill" };
|
|
1668
|
+
get themeStyle() {
|
|
1669
|
+
const base = THEME$3[this.appearance.preset ?? "mindbill"] ?? THEME$3["mindbill"];
|
|
1670
|
+
return {
|
|
1671
|
+
"--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
|
|
1672
|
+
"--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
|
|
1673
|
+
"--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
|
|
1674
|
+
"--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
|
|
1675
|
+
"--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1679
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: DashboardBase, isStandalone: true, inputs: { appearance: "appearance" }, ngImport: i0 });
|
|
1680
|
+
}
|
|
1681
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, decorators: [{
|
|
1682
|
+
type: Directive
|
|
1683
|
+
}], propDecorators: { appearance: [{
|
|
1200
1684
|
type: Input
|
|
1201
|
-
}]
|
|
1685
|
+
}] } });
|
|
1686
|
+
const sharedStyles = `
|
|
1687
|
+
: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)}
|
|
1688
|
+
.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}
|
|
1689
|
+
`;
|
|
1690
|
+
class MindBillBillAgingSummaryComponent extends DashboardBase {
|
|
1691
|
+
buckets = [];
|
|
1692
|
+
selectedBucket = "";
|
|
1693
|
+
bucketSelected = new EventEmitter();
|
|
1694
|
+
width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
|
|
1695
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1696
|
+
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" }] });
|
|
1697
|
+
}
|
|
1698
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
|
|
1699
|
+
type: Component,
|
|
1700
|
+
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"] }]
|
|
1701
|
+
}], propDecorators: { buckets: [{
|
|
1202
1702
|
type: Input
|
|
1203
|
-
}],
|
|
1703
|
+
}], selectedBucket: [{
|
|
1204
1704
|
type: Input
|
|
1205
|
-
}],
|
|
1206
|
-
type: Output
|
|
1207
|
-
}], queryChange: [{
|
|
1208
|
-
type: Output
|
|
1209
|
-
}], endReached: [{
|
|
1705
|
+
}], bucketSelected: [{
|
|
1210
1706
|
type: Output
|
|
1211
|
-
}]
|
|
1707
|
+
}] } });
|
|
1708
|
+
class MindBillBillListComponent extends DashboardBase {
|
|
1709
|
+
bills = [];
|
|
1710
|
+
billSelected = new EventEmitter();
|
|
1711
|
+
age(bill) { return mindBillAgingDays(bill); }
|
|
1712
|
+
label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
|
|
1713
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1714
|
+
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" }] });
|
|
1715
|
+
}
|
|
1716
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, decorators: [{
|
|
1717
|
+
type: Component,
|
|
1718
|
+
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"] }]
|
|
1719
|
+
}], propDecorators: { bills: [{
|
|
1720
|
+
type: Input
|
|
1721
|
+
}], billSelected: [{
|
|
1212
1722
|
type: Output
|
|
1213
1723
|
}] } });
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
const next = event.target.value;
|
|
1232
|
-
const parsed = parseMindBillSubmissionDate(next);
|
|
1233
|
-
this.display = next;
|
|
1234
|
-
this.invalid = Boolean(next && !parsed);
|
|
1235
|
-
if (!next.trim())
|
|
1236
|
-
this.valueChange.emit("");
|
|
1237
|
-
else if (parsed)
|
|
1238
|
-
this.valueChange.emit(parsed);
|
|
1239
|
-
}
|
|
1240
|
-
onBlur() {
|
|
1241
|
-
this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
|
|
1242
|
-
}
|
|
1243
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1244
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillDateInputComponent, isStandalone: true, selector: "mindbill-date-input", inputs: { value: "value", disabled: "disabled", ariaLabel: "ariaLabel" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
|
|
1245
|
-
<input
|
|
1246
|
-
type="text"
|
|
1247
|
-
inputmode="numeric"
|
|
1248
|
-
autocomplete="off"
|
|
1249
|
-
placeholder="MM/DD/YYYY"
|
|
1250
|
-
[attr.aria-label]="ariaLabel"
|
|
1251
|
-
[attr.aria-invalid]="invalid || null"
|
|
1252
|
-
[disabled]="disabled"
|
|
1253
|
-
[value]="display"
|
|
1254
|
-
(input)="onInput($event)"
|
|
1255
|
-
(blur)="onBlur()"
|
|
1256
|
-
/>
|
|
1257
|
-
@if (invalid) { <small>Use MM/DD/YYYY</small> }
|
|
1258
|
-
`, isInline: true, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
1724
|
+
class MindBillBillingDashboardComponent extends DashboardBase {
|
|
1725
|
+
bills = [];
|
|
1726
|
+
heading = "Billing dashboard";
|
|
1727
|
+
description = "Track every submitted bill from payer acceptance through payment.";
|
|
1728
|
+
billSelected = new EventEmitter();
|
|
1729
|
+
createBill = new EventEmitter();
|
|
1730
|
+
query = "";
|
|
1731
|
+
state = "";
|
|
1732
|
+
agingBucket = "";
|
|
1733
|
+
get summary() { return summarizeMindBillDashboard(this.bills); }
|
|
1734
|
+
get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
|
|
1735
|
+
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)))); }
|
|
1736
|
+
get agingLabel() { return this.summary.aging.find((item) => item.id === this.agingBucket)?.label ?? ""; }
|
|
1737
|
+
toggleAging(bucket) { this.agingBucket = this.agingBucket === bucket ? "" : bucket; }
|
|
1738
|
+
label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
|
|
1739
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1740
|
+
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" }] });
|
|
1259
1741
|
}
|
|
1260
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
1742
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
|
|
1261
1743
|
type: Component,
|
|
1262
|
-
args: [{ selector: "mindbill-
|
|
1263
|
-
|
|
1264
|
-
type="text"
|
|
1265
|
-
inputmode="numeric"
|
|
1266
|
-
autocomplete="off"
|
|
1267
|
-
placeholder="MM/DD/YYYY"
|
|
1268
|
-
[attr.aria-label]="ariaLabel"
|
|
1269
|
-
[attr.aria-invalid]="invalid || null"
|
|
1270
|
-
[disabled]="disabled"
|
|
1271
|
-
[value]="display"
|
|
1272
|
-
(input)="onInput($event)"
|
|
1273
|
-
(blur)="onBlur()"
|
|
1274
|
-
/>
|
|
1275
|
-
@if (invalid) { <small>Use MM/DD/YYYY</small> }
|
|
1276
|
-
`, styles: [":host{display:block}input{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}input[aria-invalid=true]{border-color:#c83c3c}small{display:block;margin-top:6px;color:#c83c3c;font-weight:450}\n"] }]
|
|
1277
|
-
}], propDecorators: { value: [{
|
|
1744
|
+
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"] }]
|
|
1745
|
+
}], propDecorators: { bills: [{
|
|
1278
1746
|
type: Input
|
|
1279
|
-
}],
|
|
1747
|
+
}], heading: [{
|
|
1280
1748
|
type: Input
|
|
1281
|
-
}],
|
|
1749
|
+
}], description: [{
|
|
1282
1750
|
type: Input
|
|
1283
|
-
}],
|
|
1751
|
+
}], billSelected: [{
|
|
1752
|
+
type: Output
|
|
1753
|
+
}], createBill: [{
|
|
1284
1754
|
type: Output
|
|
1285
1755
|
}] } });
|
|
1756
|
+
class MindBillBillingReportComponent extends DashboardBase {
|
|
1757
|
+
bills = [];
|
|
1758
|
+
heading = "Billing report";
|
|
1759
|
+
dimension = "status";
|
|
1760
|
+
get rows() { return buildMindBillReportRows(this.bills, this.dimension); }
|
|
1761
|
+
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); }
|
|
1762
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1763
|
+
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" }] });
|
|
1764
|
+
}
|
|
1765
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, decorators: [{
|
|
1766
|
+
type: Component,
|
|
1767
|
+
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"] }]
|
|
1768
|
+
}], propDecorators: { bills: [{
|
|
1769
|
+
type: Input
|
|
1770
|
+
}], heading: [{
|
|
1771
|
+
type: Input
|
|
1772
|
+
}] } });
|
|
1286
1773
|
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
{
|
|
1292
|
-
{ code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
|
|
1293
|
-
{ code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
|
|
1294
|
-
{ code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
|
|
1295
|
-
{ code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
|
|
1296
|
-
{ code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
|
|
1297
|
-
{ code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
|
|
1298
|
-
{ code: "90791", description: "Psychiatric diagnostic evaluation" },
|
|
1299
|
-
{ code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
|
|
1300
|
-
{ code: "90832", description: "Psychotherapy, 30 minutes" },
|
|
1301
|
-
{ code: "90834", description: "Psychotherapy, 45 minutes" },
|
|
1302
|
-
{ code: "90837", description: "Psychotherapy, 60 minutes" },
|
|
1303
|
-
{ code: "99203", description: "New patient office visit, level 3" },
|
|
1304
|
-
{ code: "99204", description: "New patient office visit, level 4" },
|
|
1305
|
-
{ code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
|
|
1306
|
-
{ code: "99213", description: "Established patient office visit, level 3" },
|
|
1307
|
-
{ code: "99214", description: "Established patient office visit, level 4" },
|
|
1308
|
-
{ code: "99215", description: "Established patient office visit, level 5" },
|
|
1309
|
-
{ code: "96130", description: "Psychological testing evaluation, first hour" },
|
|
1310
|
-
{ code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
|
|
1311
|
-
{ code: "97161", description: "Physical therapy evaluation, low complexity" },
|
|
1312
|
-
{ code: "97162", description: "Physical therapy evaluation, moderate complexity" },
|
|
1313
|
-
{ code: "97163", description: "Physical therapy evaluation, high complexity" },
|
|
1314
|
-
{ code: "97110", description: "Therapeutic exercises" },
|
|
1315
|
-
{ code: "97140", description: "Manual therapy techniques" },
|
|
1316
|
-
{ code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
|
|
1317
|
-
{ code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
|
|
1318
|
-
{ code: "72141", description: "MRI cervical spine without contrast" },
|
|
1319
|
-
{ code: "72148", description: "MRI lumbar spine without contrast" },
|
|
1320
|
-
{ code: "73721", description: "MRI lower-extremity joint without contrast" },
|
|
1321
|
-
{ code: "20610", description: "Major joint injection or aspiration" },
|
|
1322
|
-
{ code: "99070", description: "Supplies and materials" },
|
|
1323
|
-
];
|
|
1324
|
-
const MODIFIERS = [
|
|
1325
|
-
{ code: "92", description: "Primary Treating Physician evaluation" },
|
|
1326
|
-
{ code: "93", description: "Interpreter required" },
|
|
1327
|
-
{ code: "94", description: "Agreed Medical Evaluator" },
|
|
1328
|
-
{ code: "95", description: "Qualified Medical Evaluator" },
|
|
1329
|
-
{ code: "96", description: "Psychiatric/psychological evaluation" },
|
|
1330
|
-
{ code: "97", description: "Toxicology evaluation" },
|
|
1331
|
-
{ code: "98", description: "Oncology evaluation" },
|
|
1332
|
-
{ code: "59", description: "Distinct procedural service" },
|
|
1333
|
-
{ code: "XE", description: "Separate encounter" },
|
|
1334
|
-
{ code: "XS", description: "Separate structure" },
|
|
1335
|
-
{ code: "XP", description: "Separate practitioner" },
|
|
1336
|
-
{ code: "XU", description: "Unusual non-overlapping service" },
|
|
1337
|
-
];
|
|
1338
|
-
const TAXONOMIES = [
|
|
1339
|
-
{ code: "103G00000X", description: "Clinical Neuropsychologist" },
|
|
1340
|
-
{ code: "103T00000X", description: "Psychologist" },
|
|
1341
|
-
{ code: "111N00000X", description: "Chiropractor" },
|
|
1342
|
-
{ code: "207Q00000X", description: "Family Medicine" },
|
|
1343
|
-
{ code: "207R00000X", description: "Internal Medicine" },
|
|
1344
|
-
{ code: "207X00000X", description: "Orthopaedic Surgery" },
|
|
1345
|
-
{ code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
|
|
1346
|
-
{ code: "208100000X", description: "Physical Medicine & Rehabilitation" },
|
|
1347
|
-
{ code: "2083X0100X", description: "Occupational Medicine" },
|
|
1348
|
-
{ code: "2084N0400X", description: "Neurology" },
|
|
1349
|
-
{ code: "2084P0800X", description: "Psychiatry" },
|
|
1350
|
-
{ code: "208VP0000X", description: "Pain Medicine" },
|
|
1351
|
-
{ code: "213E00000X", description: "Podiatrist" },
|
|
1352
|
-
{ code: "225100000X", description: "Physical Therapist" },
|
|
1353
|
-
{ code: "225X00000X", description: "Occupational Therapist" },
|
|
1354
|
-
{ code: "363A00000X", description: "Physician Assistant" },
|
|
1355
|
-
{ code: "363L00000X", description: "Nurse Practitioner" },
|
|
1774
|
+
const MINDBILL_STATUS_AGING_BUCKETS = [
|
|
1775
|
+
{ id: "current", label: "0–30 days" },
|
|
1776
|
+
{ id: "31-60", label: "31–60 days" },
|
|
1777
|
+
{ id: "61-90", label: "61–90 days" },
|
|
1778
|
+
{ id: "91+", label: "91+ days" },
|
|
1356
1779
|
];
|
|
1357
|
-
const
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
{ label: "Neck", code: "M54.2", description: "Cervicalgia" },
|
|
1361
|
-
{ label: "Left hand", code: "M79.642", description: "Pain in left hand" },
|
|
1362
|
-
{ label: "Right hand", code: "M79.641", description: "Pain in right hand" },
|
|
1363
|
-
{ label: "Left knee", code: "M25.562", description: "Pain in left knee" },
|
|
1364
|
-
{ label: "Right knee", code: "M25.561", description: "Pain in right knee" },
|
|
1780
|
+
const DEFAULT_STATE_ORDER = [
|
|
1781
|
+
"draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
|
|
1782
|
+
"paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
|
|
1365
1783
|
];
|
|
1784
|
+
const agingDays = (bill, now) => {
|
|
1785
|
+
if (bill.agingDays != null)
|
|
1786
|
+
return Math.max(0, Math.floor(bill.agingDays));
|
|
1787
|
+
const source = bill.submittedAt ?? bill.updatedAt;
|
|
1788
|
+
if (!source)
|
|
1789
|
+
return 0;
|
|
1790
|
+
const time = new Date(source).getTime();
|
|
1791
|
+
return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
|
|
1792
|
+
};
|
|
1793
|
+
const agingBucketIndex = (days) => (days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3);
|
|
1794
|
+
const stateLabel = (state) => state.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
1795
|
+
const emptyCell = (state, bucket) => ({ state, bucket, count: 0, balance: 0, bills: [] });
|
|
1796
|
+
function buildMindBillStatusAgingMatrix(bills, stateOrder = DEFAULT_STATE_ORDER, now = new Date()) {
|
|
1797
|
+
const orderIndex = new Map(stateOrder.map((state, index) => [state.toLowerCase(), index]));
|
|
1798
|
+
const states = [...new Set(bills.map((bill) => bill.state))].sort((a, b) => {
|
|
1799
|
+
const left = orderIndex.get(a.toLowerCase()) ?? stateOrder.length;
|
|
1800
|
+
const right = orderIndex.get(b.toLowerCase()) ?? stateOrder.length;
|
|
1801
|
+
return left - right || a.localeCompare(b);
|
|
1802
|
+
});
|
|
1803
|
+
const columnTotals = MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell("all", bucket.id));
|
|
1804
|
+
const grandTotal = emptyCell("all", "total");
|
|
1805
|
+
const rows = states.map((state) => ({
|
|
1806
|
+
state,
|
|
1807
|
+
label: stateLabel(state),
|
|
1808
|
+
cells: MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell(state, bucket.id)),
|
|
1809
|
+
total: emptyCell(state, "total"),
|
|
1810
|
+
}));
|
|
1811
|
+
const rowByState = new Map(rows.map((row) => [row.state, row]));
|
|
1812
|
+
for (const bill of bills) {
|
|
1813
|
+
const row = rowByState.get(bill.state);
|
|
1814
|
+
const bucketIndex = agingBucketIndex(agingDays(bill, now));
|
|
1815
|
+
for (const cell of [row.cells[bucketIndex], row.total, columnTotals[bucketIndex], grandTotal]) {
|
|
1816
|
+
cell.count += 1;
|
|
1817
|
+
cell.balance += bill.balanceDue;
|
|
1818
|
+
cell.bills.push(bill);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return { rows, columnTotals, grandTotal };
|
|
1822
|
+
}
|
|
1823
|
+
function buildMindBillStatusAgingCsv(bills, stateOrder) {
|
|
1824
|
+
const matrix = buildMindBillStatusAgingMatrix(bills, stateOrder);
|
|
1825
|
+
const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
|
|
1826
|
+
const header = ["Status", ...MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => bucket.label), "Total"];
|
|
1827
|
+
const lines = matrix.rows.map((row) => [row.label, ...row.cells.map((cell) => cell.count), row.total.count]);
|
|
1828
|
+
lines.push(["Total", ...matrix.columnTotals.map((cell) => cell.count), matrix.grandTotal.count]);
|
|
1829
|
+
return [header, ...lines].map((line) => line.map(quote).join(",")).join("\n");
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1366
1832
|
const THEME$2 = {
|
|
1367
1833
|
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" },
|
|
1368
1834
|
"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" },
|
|
1369
1835
|
"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" },
|
|
1370
|
-
"clinical-blue": { a: "#1677ff", ac: "#fff", bg: "#
|
|
1836
|
+
"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" },
|
|
1371
1837
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
return btoa(binary);
|
|
1384
|
-
}
|
|
1385
|
-
class MindBillBillSubmissionComponent {
|
|
1386
|
-
initialBill;
|
|
1387
|
-
attachments = [];
|
|
1388
|
-
appearance = { preset: "mindbill" };
|
|
1389
|
-
sessionEndpoint = "/api/mindbill/session";
|
|
1390
|
-
getSession;
|
|
1391
|
-
apiBaseUrl;
|
|
1392
|
-
heading = "Bill information";
|
|
1393
|
-
description = "Review the bill details, add attachments, and submit.";
|
|
1394
|
-
submitLabel = "Submit bill";
|
|
1395
|
-
procedureOptions = PROCEDURES;
|
|
1396
|
-
modifierOptions = MODIFIERS;
|
|
1397
|
-
taxonomyOptions = TAXONOMIES;
|
|
1398
|
-
submitter;
|
|
1399
|
-
submitted = new EventEmitter();
|
|
1400
|
-
submissionError = new EventEmitter();
|
|
1401
|
-
billChange = new EventEmitter();
|
|
1402
|
-
bill = {};
|
|
1403
|
-
workingAttachments = [];
|
|
1404
|
-
payerQuery = "";
|
|
1405
|
-
payerResults = [];
|
|
1406
|
-
payerBusy = false;
|
|
1407
|
-
diagnosisQuery = "";
|
|
1408
|
-
diagnosisResults = [];
|
|
1409
|
-
diagnosisOffset = 0;
|
|
1410
|
-
diagnosisBusy = false;
|
|
1411
|
-
submitting = false;
|
|
1412
|
-
errorMessage = "";
|
|
1413
|
-
postalStatus = "";
|
|
1414
|
-
dragActive = false;
|
|
1415
|
-
invalidFields = new Set();
|
|
1416
|
-
quickDiagnoses = QUICK_DIAGNOSES;
|
|
1417
|
-
// Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
|
|
1418
|
-
// must schedule change detection explicitly for zone-less Angular hosts.
|
|
1419
|
-
changeDetector = inject(ChangeDetectorRef);
|
|
1420
|
-
customProcedureOption = mindBillCustomProcedureOption;
|
|
1421
|
-
customTaxonomyOption = (query) => {
|
|
1422
|
-
const code = query.trim().toUpperCase();
|
|
1423
|
-
return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
|
|
1424
|
-
};
|
|
1425
|
-
searchTimer;
|
|
1426
|
-
postalTimer;
|
|
1427
|
-
ngOnChanges(changes) {
|
|
1428
|
-
if (changes["initialBill"] && this.initialBill) {
|
|
1429
|
-
this.bill = deepCopyBill(this.initialBill);
|
|
1430
|
-
if (!this.bill.serviceLines.length)
|
|
1431
|
-
this.bill.serviceLines = [{ code: "", units: 1 }];
|
|
1432
|
-
this.ensureTrailingLine();
|
|
1433
|
-
this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
|
|
1434
|
-
}
|
|
1435
|
-
if (changes["attachments"])
|
|
1436
|
-
this.workingAttachments = this.attachments.map((item) => ({ ...item }));
|
|
1838
|
+
class MindBillStatusAgingMatrixComponent {
|
|
1839
|
+
bills = [];
|
|
1840
|
+
heading = "Bills by status and age";
|
|
1841
|
+
description = "Every bill grouped by lifecycle status, then by days outstanding.";
|
|
1842
|
+
stateOrder;
|
|
1843
|
+
showBalances = true;
|
|
1844
|
+
appearance = { preset: "mindbill" };
|
|
1845
|
+
cellSelected = new EventEmitter();
|
|
1846
|
+
buckets = MINDBILL_STATUS_AGING_BUCKETS;
|
|
1847
|
+
get matrix() {
|
|
1848
|
+
return buildMindBillStatusAgingMatrix(this.bills, this.stateOrder);
|
|
1437
1849
|
}
|
|
1438
1850
|
get themeStyle() {
|
|
1439
1851
|
const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
|
|
@@ -1445,547 +1857,282 @@ class MindBillBillSubmissionComponent {
|
|
|
1445
1857
|
"--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
|
|
1446
1858
|
};
|
|
1447
1859
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
if (path === "patient.dateOfBirth")
|
|
1464
|
-
this.bill.patient.dateOfBirth = value;
|
|
1465
|
-
else if (path === "service.date")
|
|
1466
|
-
this.bill.service.date = value;
|
|
1467
|
-
else
|
|
1468
|
-
this.bill.claim.dateOfInjury = value;
|
|
1469
|
-
if (value)
|
|
1470
|
-
this.invalidFields.delete(path);
|
|
1471
|
-
this.billChange.emit(this.bill);
|
|
1472
|
-
}
|
|
1473
|
-
postalChanged(value) {
|
|
1474
|
-
clearTimeout(this.postalTimer);
|
|
1475
|
-
this.postalStatus = "";
|
|
1476
|
-
if (!/^\d{5}$/.test(value.trim()))
|
|
1477
|
-
return;
|
|
1478
|
-
this.postalStatus = "Looking up ZIP…";
|
|
1479
|
-
this.postalTimer = setTimeout(async () => {
|
|
1480
|
-
try {
|
|
1481
|
-
const place = await this.referenceClient().lookupPostalCode(value);
|
|
1482
|
-
if (place) {
|
|
1483
|
-
this.bill.patient.address.city = place.city;
|
|
1484
|
-
this.bill.patient.address.state = place.state.toUpperCase();
|
|
1485
|
-
this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
|
|
1486
|
-
this.invalidFields.delete("patient.address.city");
|
|
1487
|
-
this.invalidFields.delete("patient.address.state");
|
|
1488
|
-
this.billChange.emit(this.bill);
|
|
1489
|
-
}
|
|
1490
|
-
else {
|
|
1491
|
-
this.postalStatus = "ZIP not found";
|
|
1860
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1861
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillStatusAgingMatrixComponent, isStandalone: true, selector: "mindbill-status-aging-matrix", inputs: { bills: "bills", heading: "heading", description: "description", stateOrder: "stateOrder", showBalances: "showBalances", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
|
|
1862
|
+
<section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
|
|
1863
|
+
<header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
|
|
1864
|
+
<div class="card">
|
|
1865
|
+
<table>
|
|
1866
|
+
<thead>
|
|
1867
|
+
<tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
|
|
1868
|
+
</thead>
|
|
1869
|
+
<tbody>
|
|
1870
|
+
@for (row of matrix.rows; track row.state) {
|
|
1871
|
+
<tr>
|
|
1872
|
+
<td class="status"><i class="pill">{{ row.label }}</i></td>
|
|
1873
|
+
@for (cell of row.cells; track cell.bucket) {
|
|
1874
|
+
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
1492
1875
|
}
|
|
1876
|
+
<td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
|
|
1877
|
+
</tr>
|
|
1493
1878
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if (reset) {
|
|
1527
|
-
this.diagnosisOffset = 0;
|
|
1528
|
-
this.diagnosisResults = [];
|
|
1529
|
-
}
|
|
1530
|
-
clearTimeout(this.searchTimer);
|
|
1531
|
-
this.searchTimer = setTimeout(async () => {
|
|
1532
|
-
this.diagnosisBusy = true;
|
|
1533
|
-
try {
|
|
1534
|
-
const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
|
|
1535
|
-
this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
|
|
1536
|
-
this.diagnosisOffset = this.diagnosisResults.length;
|
|
1537
|
-
}
|
|
1538
|
-
catch {
|
|
1539
|
-
if (reset)
|
|
1540
|
-
this.diagnosisResults = [];
|
|
1541
|
-
}
|
|
1542
|
-
finally {
|
|
1543
|
-
this.diagnosisBusy = false;
|
|
1544
|
-
this.changeDetector.markForCheck();
|
|
1879
|
+
<tr class="totals">
|
|
1880
|
+
<td class="status">Total</td>
|
|
1881
|
+
@for (cell of matrix.columnTotals; track cell.bucket) {
|
|
1882
|
+
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
1883
|
+
}
|
|
1884
|
+
<td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
|
|
1885
|
+
</tr>
|
|
1886
|
+
</tbody>
|
|
1887
|
+
</table>
|
|
1888
|
+
</div>
|
|
1889
|
+
</section>
|
|
1890
|
+
`, isInline: true, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
|
|
1891
|
+
}
|
|
1892
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, decorators: [{
|
|
1893
|
+
type: Component,
|
|
1894
|
+
args: [{ selector: "mindbill-status-aging-matrix", standalone: true, imports: [CommonModule], template: `
|
|
1895
|
+
<section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
|
|
1896
|
+
<header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
|
|
1897
|
+
<div class="card">
|
|
1898
|
+
<table>
|
|
1899
|
+
<thead>
|
|
1900
|
+
<tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
|
|
1901
|
+
</thead>
|
|
1902
|
+
<tbody>
|
|
1903
|
+
@for (row of matrix.rows; track row.state) {
|
|
1904
|
+
<tr>
|
|
1905
|
+
<td class="status"><i class="pill">{{ row.label }}</i></td>
|
|
1906
|
+
@for (cell of row.cells; track cell.bucket) {
|
|
1907
|
+
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
1908
|
+
}
|
|
1909
|
+
<td class="rowtotal"><button type="button" [disabled]="row.total.count === 0" (click)="cellSelected.emit(row.total)"><b>{{ row.total.count }}</b>@if (showBalances) { <small>{{ row.total.balance | currency }}</small> }</button></td>
|
|
1910
|
+
</tr>
|
|
1545
1911
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
:
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
this.billChange.emit(this.bill);
|
|
1600
|
-
}
|
|
1601
|
-
selectTaxonomy(option) {
|
|
1602
|
-
this.bill.renderingProvider.taxonomy = option.id;
|
|
1603
|
-
this.invalidFields.delete("renderingProvider.taxonomy");
|
|
1604
|
-
this.billChange.emit(this.bill);
|
|
1605
|
-
}
|
|
1606
|
-
procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
|
|
1607
|
-
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; }
|
|
1608
|
-
get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
|
|
1609
|
-
onWindowDragOver(event) {
|
|
1610
|
-
if (event.dataTransfer?.types.includes("Files")) {
|
|
1611
|
-
event.preventDefault();
|
|
1612
|
-
this.dragActive = true;
|
|
1613
|
-
this.changeDetector.markForCheck();
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
onWindowDragLeave(event) {
|
|
1617
|
-
if (!event.relatedTarget) {
|
|
1618
|
-
this.dragActive = false;
|
|
1619
|
-
this.changeDetector.markForCheck();
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
onWindowDrop(event) {
|
|
1623
|
-
if (event.dataTransfer?.files.length) {
|
|
1624
|
-
event.preventDefault();
|
|
1625
|
-
this.dragActive = false;
|
|
1626
|
-
void this.addFiles(event.dataTransfer.files);
|
|
1627
|
-
this.changeDetector.markForCheck();
|
|
1628
|
-
}
|
|
1912
|
+
<tr class="totals">
|
|
1913
|
+
<td class="status">Total</td>
|
|
1914
|
+
@for (cell of matrix.columnTotals; track cell.bucket) {
|
|
1915
|
+
<td><button type="button" [disabled]="cell.count === 0" (click)="cellSelected.emit(cell)"><b [class.zero]="cell.count === 0">{{ cell.count === 0 ? '—' : cell.count }}</b>@if (showBalances && cell.count > 0) { <small>{{ cell.balance | currency }}</small> }</button></td>
|
|
1916
|
+
}
|
|
1917
|
+
<td class="rowtotal"><button type="button" [disabled]="matrix.grandTotal.count === 0" (click)="cellSelected.emit(matrix.grandTotal)"><b>{{ matrix.grandTotal.count }}</b>@if (showBalances) { <small>{{ matrix.grandTotal.balance | currency }}</small> }</button></td>
|
|
1918
|
+
</tr>
|
|
1919
|
+
</tbody>
|
|
1920
|
+
</table>
|
|
1921
|
+
</div>
|
|
1922
|
+
</section>
|
|
1923
|
+
`, styles: [":host{display:block}.mbmx{font-family:var(--font);color:var(--t)}.mbmx header{margin-bottom:16px}.mbmx h2{margin:0;font-size:24px}.mbmx header p{margin:5px 0 0;color:var(--m)}.card{background:var(--s);border:1px solid var(--b);border-radius:var(--r);overflow:auto}table{width:100%;border-collapse:collapse;min-width:640px}th{padding:13px 16px;border-bottom:1px solid var(--b);color:var(--m);font-size:11px;letter-spacing:.06em;text-transform:uppercase;text-align:right;white-space:nowrap}th.status{text-align:left}td{padding:0;border-bottom:1px solid var(--b);border-left:1px solid color-mix(in srgb,var(--b) 55%,transparent);text-align:right}td.status{border-left:0;padding:14px 16px;text-align:left;white-space:nowrap}tr:last-child td{border-bottom:0}.pill{display:inline-flex;border-radius:999px;background:color-mix(in srgb,var(--a) 12%,var(--s));color:var(--a);font-size:12px;font-style:normal;font-weight:750;padding:5px 9px}td button{display:block;width:100%;min-height:56px;padding:9px 16px;border:0;background:none;color:var(--t);font:inherit;text-align:right;cursor:pointer}td button:disabled{cursor:default}td button:not(:disabled):hover{background:color-mix(in srgb,var(--a) 8%,var(--s))}td button b{display:block;font-size:17px;font-weight:760;font-variant-numeric:tabular-nums}td button b.zero{color:var(--m);font-weight:500}td button small{display:block;margin-top:2px;color:var(--m);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}td.rowtotal,tr.totals td{background:color-mix(in srgb,var(--bg) 78%,var(--s));font-weight:750}tr.totals td.status{color:var(--t);font-weight:750}\n"] }]
|
|
1924
|
+
}], propDecorators: { bills: [{
|
|
1925
|
+
type: Input
|
|
1926
|
+
}], heading: [{
|
|
1927
|
+
type: Input
|
|
1928
|
+
}], description: [{
|
|
1929
|
+
type: Input
|
|
1930
|
+
}], stateOrder: [{
|
|
1931
|
+
type: Input
|
|
1932
|
+
}], showBalances: [{
|
|
1933
|
+
type: Input
|
|
1934
|
+
}], appearance: [{
|
|
1935
|
+
type: Input
|
|
1936
|
+
}], cellSelected: [{
|
|
1937
|
+
type: Output
|
|
1938
|
+
}] } });
|
|
1939
|
+
|
|
1940
|
+
const MINDBILL_BILL_TASKS_TONES = {
|
|
1941
|
+
violet: "#7c53c3",
|
|
1942
|
+
red: "#cf4437",
|
|
1943
|
+
blue: "#2f7fd1",
|
|
1944
|
+
green: "#2c9a5b",
|
|
1945
|
+
amber: "#d9931f",
|
|
1946
|
+
neutral: "#8195a1",
|
|
1947
|
+
};
|
|
1948
|
+
const MINDBILL_BILL_TASKS_PILL_BASES = ["#2c9a5b", "#1f9aa8", "#dcbb28", "#e08a2e", "#d75d8a"];
|
|
1949
|
+
function mindBillBillTasksTone(tone) {
|
|
1950
|
+
return MINDBILL_BILL_TASKS_TONES[tone] ?? MINDBILL_BILL_TASKS_TONES.neutral;
|
|
1951
|
+
}
|
|
1952
|
+
function mindBillBillTasksPillBase(index) {
|
|
1953
|
+
return MINDBILL_BILL_TASKS_PILL_BASES[index % MINDBILL_BILL_TASKS_PILL_BASES.length];
|
|
1954
|
+
}
|
|
1955
|
+
/** Click-through payload for a bucket cell (bucketIndex) or a row total (null). */
|
|
1956
|
+
function mindBillBillTasksCell(section, row, bucketIndex, buckets) {
|
|
1957
|
+
if (bucketIndex === null) {
|
|
1958
|
+
return {
|
|
1959
|
+
sectionId: section.id,
|
|
1960
|
+
rowId: row.id,
|
|
1961
|
+
bucketId: null,
|
|
1962
|
+
refs: row.refs.flat(),
|
|
1963
|
+
count: row.total,
|
|
1964
|
+
};
|
|
1629
1965
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1966
|
+
return {
|
|
1967
|
+
sectionId: section.id,
|
|
1968
|
+
rowId: row.id,
|
|
1969
|
+
bucketId: buckets[bucketIndex]?.id ?? null,
|
|
1970
|
+
refs: row.refs[bucketIndex] ?? [],
|
|
1971
|
+
count: row.counts[bucketIndex] ?? 0,
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// daisyBill-style "Bill Tasks" dashboard: one rounded, tone-colored card per
|
|
1976
|
+
// task section, rows bucketed by age in days with colored bucket header pills,
|
|
1977
|
+
// per-cell click-through counts, and a closing grand-total card. The pure
|
|
1978
|
+
// aggregation lives in @mindbill/browser (buildBillTasksDashboard) so React
|
|
1979
|
+
// and Angular render the exact same data shape.
|
|
1980
|
+
class MindBillBillTasksDashboardComponent {
|
|
1981
|
+
data = null;
|
|
1982
|
+
buckets = BILL_TASKS_AGING_BUCKETS;
|
|
1983
|
+
heading = "";
|
|
1984
|
+
grandTotalLabel = "Bill Tasks Total";
|
|
1985
|
+
emptyLabel = "No Tasks";
|
|
1986
|
+
appearance = { preset: "mindbill" };
|
|
1987
|
+
cellSelected = new EventEmitter();
|
|
1988
|
+
get themeStyle() {
|
|
1989
|
+
return {
|
|
1990
|
+
...mindBillAngularAppearanceStyle(this.appearance),
|
|
1991
|
+
"--cols": `minmax(150px,1.6fr) repeat(${this.buckets.length},minmax(84px,1fr)) minmax(84px,1fr)`,
|
|
1992
|
+
};
|
|
1657
1993
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
await this.addFiles(input.files ?? []);
|
|
1661
|
-
input.value = "";
|
|
1994
|
+
tone(section) {
|
|
1995
|
+
return mindBillBillTasksTone(section.tone);
|
|
1662
1996
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
previewAttachment(attachment) { if (attachment.contentUrl)
|
|
1666
|
-
window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
|
|
1667
|
-
else if (attachment.contentBase64)
|
|
1668
|
-
window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer"); }
|
|
1669
|
-
bad(path) { return this.invalidFields.has(path); }
|
|
1670
|
-
validate() {
|
|
1671
|
-
const candidates = [
|
|
1672
|
-
["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
|
|
1673
|
-
["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],
|
|
1674
|
-
["service.date", parseMindBillSubmissionDate(this.bill.service.date ?? "")], ["claim.dateOfInjury", parseMindBillSubmissionDate(this.bill.claim.dateOfInjury ?? "")], ["claim.claimNumber", this.bill.claim.claimNumber], ["claim.employer", this.bill.claim.employer],
|
|
1675
|
-
["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
|
|
1676
|
-
["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
|
|
1677
|
-
["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],
|
|
1678
|
-
["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
|
|
1679
|
-
["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],
|
|
1680
|
-
["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
|
|
1681
|
-
];
|
|
1682
|
-
this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
|
|
1683
|
-
if (this.invalidFields.size) {
|
|
1684
|
-
this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
|
|
1685
|
-
setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
|
|
1686
|
-
return false;
|
|
1687
|
-
}
|
|
1688
|
-
return true;
|
|
1997
|
+
pillBackground(index) {
|
|
1998
|
+
return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 20%,var(--s))`;
|
|
1689
1999
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
for (const attachment of this.workingAttachments) {
|
|
1693
|
-
let contentBase64 = attachment.contentBase64;
|
|
1694
|
-
if (!contentBase64 && attachment.contentUrl) {
|
|
1695
|
-
const response = await fetch(attachment.contentUrl);
|
|
1696
|
-
if (!response.ok)
|
|
1697
|
-
throw new Error(`Could not load ${attachment.filename}.`);
|
|
1698
|
-
contentBase64 = await blobToBase64(await response.blob());
|
|
1699
|
-
}
|
|
1700
|
-
if (!contentBase64)
|
|
1701
|
-
continue;
|
|
1702
|
-
documents.push({
|
|
1703
|
-
filename: attachment.filename,
|
|
1704
|
-
documentType: attachment.documentType,
|
|
1705
|
-
contentBase64,
|
|
1706
|
-
...(attachment.externalId ? { externalId: attachment.externalId } : {}),
|
|
1707
|
-
...(attachment.description ? { description: attachment.description } : {}),
|
|
1708
|
-
...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
return documents;
|
|
2000
|
+
pillColor(index) {
|
|
2001
|
+
return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 62%,var(--t))`;
|
|
1712
2002
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
this.errorMessage = "";
|
|
1716
|
-
if (!this.validate())
|
|
1717
|
-
return;
|
|
1718
|
-
this.submitting = true;
|
|
1719
|
-
try {
|
|
1720
|
-
const bill = deepCopyBill(this.bill);
|
|
1721
|
-
bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
|
|
1722
|
-
bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
|
|
1723
|
-
bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
|
|
1724
|
-
bill.serviceLines = bill.serviceLines
|
|
1725
|
-
.filter((line) => line.code.trim())
|
|
1726
|
-
.map((line) => {
|
|
1727
|
-
const charge = line.charge ?? this.allowed(line);
|
|
1728
|
-
return charge == null ? line : { ...line, charge };
|
|
1729
|
-
});
|
|
1730
|
-
const input = { bill, documents: await this.submissionDocuments() };
|
|
1731
|
-
const result = this.submitter
|
|
1732
|
-
? await this.submitter(input)
|
|
1733
|
-
: await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
|
|
1734
|
-
this.submitted.emit(result);
|
|
1735
|
-
}
|
|
1736
|
-
catch (value) {
|
|
1737
|
-
const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
|
|
1738
|
-
this.errorMessage = error.message;
|
|
1739
|
-
this.submissionError.emit(error);
|
|
1740
|
-
}
|
|
1741
|
-
finally {
|
|
1742
|
-
this.submitting = false;
|
|
1743
|
-
this.changeDetector.markForCheck();
|
|
1744
|
-
}
|
|
2003
|
+
select(section, row, bucketIndex) {
|
|
2004
|
+
this.cellSelected.emit(mindBillBillTasksCell(section, row, bucketIndex, this.buckets));
|
|
1745
2005
|
}
|
|
1746
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
1747
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type:
|
|
1748
|
-
<
|
|
1749
|
-
<
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
<fieldset id="mbs-claim"><legend>Injury & claim</legend><div class="grid">
|
|
1763
|
-
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
|
|
1764
|
-
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
|
|
1765
|
-
<label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
|
|
1766
|
-
<label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
|
|
1767
|
-
<label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
|
|
1768
|
-
<div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
|
|
1769
|
-
<input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
|
|
1770
|
-
@if (payerBusy) { <small>Searching directory…</small> }
|
|
1771
|
-
@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> }
|
|
1772
|
-
@if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
|
|
1773
|
-
</div>
|
|
1774
|
-
<label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
|
|
1775
|
-
<div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
|
|
1776
|
-
<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>
|
|
1777
|
-
@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> }
|
|
1778
|
-
<input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
|
|
1779
|
-
@if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
|
|
1780
|
-
@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> }
|
|
1781
|
-
</div>
|
|
1782
|
-
</div></fieldset>
|
|
1783
|
-
|
|
1784
|
-
<fieldset id="mbs-provider"><legend>Providers & place of service</legend><div class="grid">
|
|
1785
|
-
<h3 class="wide">Billing provider</h3>
|
|
1786
|
-
<label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
|
|
1787
|
-
<label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
|
|
1788
|
-
<label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1789
|
-
<label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
|
|
1790
|
-
<label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
|
|
1791
|
-
<label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
|
|
1792
|
-
<label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
|
|
1793
|
-
<label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
|
|
1794
|
-
<h3 class="wide">Rendering provider</h3>
|
|
1795
|
-
<label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
|
|
1796
|
-
<label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1797
|
-
<div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
|
|
1798
|
-
<mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
|
|
1799
|
-
<small>Search by specialty name or 10-character taxonomy code.</small>
|
|
1800
|
-
</div>
|
|
1801
|
-
<h3 class="wide">Place of service</h3>
|
|
1802
|
-
<label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
|
|
1803
|
-
<label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
|
|
1804
|
-
<label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
|
|
1805
|
-
<label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
|
|
1806
|
-
<label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
|
|
1807
|
-
</div></fieldset>
|
|
1808
|
-
|
|
1809
|
-
<fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
|
|
1810
|
-
<div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
|
|
1811
|
-
@for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
|
|
1812
|
-
<div class="cell"><span class="mobile-label">Procedure</span>
|
|
1813
|
-
<mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
|
|
1814
|
-
@if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
|
|
1815
|
-
</div>
|
|
1816
|
-
<div class="cell"><span class="mobile-label">Modifiers</span>
|
|
1817
|
-
@if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
|
|
1818
|
-
<mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
|
|
2006
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2007
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillTasksDashboardComponent, isStandalone: true, selector: "mindbill-bill-tasks-dashboard", inputs: { data: "data", buckets: "buckets", heading: "heading", grandTotalLabel: "grandTotalLabel", emptyLabel: "emptyLabel", appearance: "appearance" }, outputs: { cellSelected: "cellSelected" }, ngImport: i0, template: `
|
|
2008
|
+
<div class="mbtk" [ngStyle]="themeStyle">
|
|
2009
|
+
@if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
|
|
2010
|
+
@for (section of data?.sections ?? []; track section.id) {
|
|
2011
|
+
<section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
|
|
2012
|
+
<div class="grid head" [style.borderBottomColor]="tone(section)">
|
|
2013
|
+
<span class="title">
|
|
2014
|
+
<i class="dot" [style.background]="tone(section)"></i>
|
|
2015
|
+
<span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
|
|
2016
|
+
</span>
|
|
2017
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2018
|
+
<span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
|
|
2019
|
+
}
|
|
2020
|
+
<span class="colhead">Task Total</span>
|
|
1819
2021
|
</div>
|
|
1820
|
-
|
|
1821
|
-
<
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2022
|
+
@if (section.empty) {
|
|
2023
|
+
<p class="empty">{{ emptyLabel }}</p>
|
|
2024
|
+
} @else {
|
|
2025
|
+
@for (row of section.rows; track row.id) {
|
|
2026
|
+
<div class="grid row">
|
|
2027
|
+
<span class="rowlabel">{{ row.label }}</span>
|
|
2028
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2029
|
+
<span class="count">
|
|
2030
|
+
@if ((row.counts[bucketIndex] ?? 0) > 0) {
|
|
2031
|
+
<button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
|
|
2032
|
+
} @else { <span class="zero">0</span> }
|
|
2033
|
+
</span>
|
|
1825
2034
|
}
|
|
1826
|
-
|
|
2035
|
+
<span class="total">
|
|
2036
|
+
@if (row.total > 0) {
|
|
2037
|
+
<button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
|
|
2038
|
+
} @else { <span class="zero">0</span> }
|
|
2039
|
+
</span>
|
|
2040
|
+
</div>
|
|
2041
|
+
}
|
|
2042
|
+
<div class="grid row totals">
|
|
2043
|
+
<span class="rowlabel">Total</span>
|
|
2044
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2045
|
+
<span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
|
|
2046
|
+
}
|
|
2047
|
+
<span class="total">{{ section.total }}</span>
|
|
1827
2048
|
</div>
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
</
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
<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>
|
|
1843
|
-
@if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
|
|
1844
|
-
</form>
|
|
1845
|
-
`, isInline: true, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MindBillComboBoxComponent, selector: "mindbill-combo-box", inputs: ["value", "placeholder", "options", "disabled", "loading", "loadingMore", "invalid", "preserveValueOnOpen", "ariaLabel", "createOption"], outputs: ["opened", "queryChange", "endReached", "selected"] }, { kind: "component", type: MindBillDateInputComponent, selector: "mindbill-date-input", inputs: ["value", "disabled", "ariaLabel"], outputs: ["valueChange"] }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }] });
|
|
2049
|
+
}
|
|
2050
|
+
</section>
|
|
2051
|
+
}
|
|
2052
|
+
<section class="card grand" [attr.aria-label]="grandTotalLabel">
|
|
2053
|
+
<div class="grid row totals">
|
|
2054
|
+
<span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
|
|
2055
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2056
|
+
<span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
|
|
2057
|
+
}
|
|
2058
|
+
<span class="total">{{ data?.grandTotal ?? 0 }}</span>
|
|
2059
|
+
</div>
|
|
2060
|
+
</section>
|
|
2061
|
+
</div>
|
|
2062
|
+
`, isInline: true, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
|
|
1846
2063
|
}
|
|
1847
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type:
|
|
2064
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, decorators: [{
|
|
1848
2065
|
type: Component,
|
|
1849
|
-
args: [{ selector: "mindbill-bill-
|
|
1850
|
-
<
|
|
1851
|
-
<
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
<fieldset id="mbs-claim"><legend>Injury & claim</legend><div class="grid">
|
|
1865
|
-
<label [class.invalid]="bad('service.date')">Date of service <b>*</b><mindbill-date-input ariaLabel="Date of service" [value]="bill.service.date" (valueChange)="setDate('service.date', $event)"/></label>
|
|
1866
|
-
<label [class.invalid]="bad('claim.dateOfInjury')">Date of injury <b>*</b><mindbill-date-input ariaLabel="Date of injury" [value]="bill.claim.dateOfInjury" (valueChange)="setDate('claim.dateOfInjury', $event)"/></label>
|
|
1867
|
-
<label class="wide">Treatment authorization # (optional)<input name="authNumber" [ngModel]="bill.service.authorizationNumber || ''" (ngModelChange)="setAuthorizationNumber($event)" placeholder="Utilization-review authorization number"><small>Rides on the bill as CMS-1500 Box 23 / 837 REF*G1.</small></label>
|
|
1868
|
-
<label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
|
|
1869
|
-
<label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
|
|
1870
|
-
<div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
|
|
1871
|
-
<input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
|
|
1872
|
-
@if (payerBusy) { <small>Searching directory…</small> }
|
|
1873
|
-
@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> }
|
|
1874
|
-
@if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
|
|
1875
|
-
</div>
|
|
1876
|
-
<label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
|
|
1877
|
-
<div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
|
|
1878
|
-
<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>
|
|
1879
|
-
@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> }
|
|
1880
|
-
<input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
|
|
1881
|
-
@if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
|
|
1882
|
-
@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> }
|
|
1883
|
-
</div>
|
|
1884
|
-
</div></fieldset>
|
|
1885
|
-
|
|
1886
|
-
<fieldset id="mbs-provider"><legend>Providers & place of service</legend><div class="grid">
|
|
1887
|
-
<h3 class="wide">Billing provider</h3>
|
|
1888
|
-
<label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
|
|
1889
|
-
<label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
|
|
1890
|
-
<label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1891
|
-
<label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
|
|
1892
|
-
<label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
|
|
1893
|
-
<label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
|
|
1894
|
-
<label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
|
|
1895
|
-
<label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
|
|
1896
|
-
<h3 class="wide">Rendering provider</h3>
|
|
1897
|
-
<label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
|
|
1898
|
-
<label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
|
|
1899
|
-
<div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
|
|
1900
|
-
<mindbill-combo-box ariaLabel="Rendering taxonomy" [value]="bill.renderingProvider.taxonomy || ''" [invalid]="bad('renderingProvider.taxonomy')" [preserveValueOnOpen]="true" placeholder="Search specialty name or taxonomy code…" [options]="taxonomyComboOptions" [createOption]="customTaxonomyOption" (selected)="selectTaxonomy($event)"/>
|
|
1901
|
-
<small>Search by specialty name or 10-character taxonomy code.</small>
|
|
1902
|
-
</div>
|
|
1903
|
-
<h3 class="wide">Place of service</h3>
|
|
1904
|
-
<label [class.invalid]="bad('serviceLocation.placeOfServiceCode')">Place of service code <b>*</b><input name="pos" [(ngModel)]="bill.serviceLocation.placeOfServiceCode" placeholder="11" inputmode="numeric" maxlength="2"></label>
|
|
1905
|
-
<label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
|
|
1906
|
-
<label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
|
|
1907
|
-
<label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
|
|
1908
|
-
<label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
|
|
1909
|
-
</div></fieldset>
|
|
1910
|
-
|
|
1911
|
-
<fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
|
|
1912
|
-
<div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
|
|
1913
|
-
@for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
|
|
1914
|
-
<div class="cell"><span class="mobile-label">Procedure</span>
|
|
1915
|
-
<mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
|
|
1916
|
-
@if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
|
|
1917
|
-
</div>
|
|
1918
|
-
<div class="cell"><span class="mobile-label">Modifiers</span>
|
|
1919
|
-
@if (line.modifiers?.length) { <div class="chips">@for (modifier of line.modifiers; track modifier) { <button type="button" (click)="removeModifier(index, modifier)" [attr.aria-label]="'Remove modifier ' + modifier">−{{ modifier }} ×</button> }</div> }
|
|
1920
|
-
<mindbill-combo-box [ariaLabel]="'Modifiers ' + (index + 1)" value="" [placeholder]="line.modifiers?.length ? line.modifiers!.length + ' modifier' + (line.modifiers!.length === 1 ? '' : 's') : 'Add modifiers…'" [options]="modifierComboOptions(line)" (selected)="addModifier(index, $event)"/>
|
|
2066
|
+
args: [{ selector: "mindbill-bill-tasks-dashboard", standalone: true, imports: [CommonModule], template: `
|
|
2067
|
+
<div class="mbtk" [ngStyle]="themeStyle">
|
|
2068
|
+
@if (heading) { <header class="heading"><h2>{{ heading }}</h2></header> }
|
|
2069
|
+
@for (section of data?.sections ?? []; track section.id) {
|
|
2070
|
+
<section class="card" [style.borderColor]="tone(section)" [attr.aria-label]="section.label">
|
|
2071
|
+
<div class="grid head" [style.borderBottomColor]="tone(section)">
|
|
2072
|
+
<span class="title">
|
|
2073
|
+
<i class="dot" [style.background]="tone(section)"></i>
|
|
2074
|
+
<span class="title-text"><strong>{{ section.label }}</strong><span class="basis">by {{ section.agingBasisLabel }}</span></span>
|
|
2075
|
+
</span>
|
|
2076
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2077
|
+
<span class="pill" [style.background]="pillBackground(bucketIndex)" [style.color]="pillColor(bucketIndex)">{{ bucket.label }}</span>
|
|
2078
|
+
}
|
|
2079
|
+
<span class="colhead">Task Total</span>
|
|
1921
2080
|
</div>
|
|
1922
|
-
|
|
1923
|
-
<
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2081
|
+
@if (section.empty) {
|
|
2082
|
+
<p class="empty">{{ emptyLabel }}</p>
|
|
2083
|
+
} @else {
|
|
2084
|
+
@for (row of section.rows; track row.id) {
|
|
2085
|
+
<div class="grid row">
|
|
2086
|
+
<span class="rowlabel">{{ row.label }}</span>
|
|
2087
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2088
|
+
<span class="count">
|
|
2089
|
+
@if ((row.counts[bucketIndex] ?? 0) > 0) {
|
|
2090
|
+
<button type="button" class="link" (click)="select(section, row, bucketIndex)">{{ row.counts[bucketIndex] }}</button>
|
|
2091
|
+
} @else { <span class="zero">0</span> }
|
|
2092
|
+
</span>
|
|
1927
2093
|
}
|
|
1928
|
-
|
|
2094
|
+
<span class="total">
|
|
2095
|
+
@if (row.total > 0) {
|
|
2096
|
+
<button type="button" class="link" (click)="select(section, row, null)">{{ row.total }}</button>
|
|
2097
|
+
} @else { <span class="zero">0</span> }
|
|
2098
|
+
</span>
|
|
2099
|
+
</div>
|
|
2100
|
+
}
|
|
2101
|
+
<div class="grid row totals">
|
|
2102
|
+
<span class="rowlabel">Total</span>
|
|
2103
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2104
|
+
<span class="count">{{ section.totals[bucketIndex] ?? 0 }}</span>
|
|
2105
|
+
}
|
|
2106
|
+
<span class="total">{{ section.total }}</span>
|
|
1929
2107
|
</div>
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
</
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
@if (dragActive) { <div class="drop-overlay" aria-hidden="true"><div><strong>Drop PDFs to attach</strong><span>Supporting documents are added to this submission.</span></div></div> }
|
|
1946
|
-
</form>
|
|
1947
|
-
`, styles: [":host{display:block}.mbs{--danger:#c83c3c;font-family:var(--font);color:var(--t)}*{box-sizing:border-box}.intro{display:flex;justify-content:space-between;align-items:start;margin:0 0 24px}.intro h2{margin:0;font-size:24px;font-weight:600}.intro p{margin:6px 0 0;color:var(--m)}.intro>span{color:var(--m);font-size:13px}.mbs b{color:inherit}.intro b,label>b,.lookup>label>b,.diagnosis>label>b,.cell>label>b{color:var(--danger)}fieldset{border:1px solid var(--b);border-radius:var(--r);margin:0 0 22px;padding:24px 26px 26px;background:var(--s)}legend{padding:0 10px;font-size:18px;font-weight:700}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 28px}.wide{grid-column:1/-1}h3{margin:4px 0 -4px;padding-top:12px;border-top:1px solid var(--b);font-size:16px}h3:first-child{border:0;padding-top:0}label,.lookup,.diagnosis{display:grid;gap:8px;font-size:14px;font-weight:700}.grid>label{display:flex;flex-wrap:wrap;align-content:start;align-items:center;column-gap:4px;row-gap:8px}.grid>label>input,.grid>label>select,.grid>label>small,.grid>label>mindbill-date-input{flex:0 0 100%}.grid>div.wide{display:grid;gap:8px;font-size:14px;font-weight:700}input,select{width:100%;min-height:46px;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:10px 12px;color:var(--t);font:inherit;font-weight:450}input:focus,select:focus{outline:3px solid color-mix(in srgb,var(--a) 22%,transparent);border-color:var(--a)}.invalid input,.invalid select,.invalid-set{border-color:var(--danger)!important;background:color-mix(in srgb,var(--danger) 4%,var(--s))}.invalid>label{color:var(--danger)}small{color:var(--m);font-weight:450}.selected{color:#238655}.options{position:relative;z-index:4;max-height:300px;overflow:auto;border:1px solid var(--b);border-radius:var(--cr);background:var(--s);box-shadow:0 14px 35px #172b3730}.options button{display:grid;width:100%;gap:3px;border:0;border-bottom:1px solid var(--b);background:var(--s);padding:12px 14px;text-align:left;color:var(--t);cursor:pointer}.options button:hover{background:color-mix(in srgb,var(--a) 7%,var(--s))}.options span{color:var(--m);font-size:12px}.quick,.chips{display:flex;flex-wrap:wrap;gap:8px}.quick button,.chips button{border:1px solid var(--b);border-radius:999px;background:var(--s);padding:7px 11px;color:var(--t);font:inherit;font-size:13px;cursor:pointer}.quick .on{border-color:var(--a);color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.chips button{border-color:color-mix(in srgb,var(--a) 40%,var(--b));background:color-mix(in srgb,var(--a) 7%,var(--s));font-weight:700}.dx{display:flex;flex-wrap:wrap;gap:5px;padding-top:6px}.dx button{width:30px;height:30px;border:1px solid var(--b);border-radius:8px;background:var(--s);color:var(--m);font:inherit;font-size:13px;font-weight:750;cursor:pointer;padding:0}.dx button.on{border-color:var(--a);background:color-mix(in srgb,var(--a) 10%,var(--s));color:var(--a)}.line-head,.service-line{display:grid;grid-template-columns:1.5fr 1.35fr .8fr .5fr .7fr 36px;gap:14px;align-items:start}.line-head{padding:0 0 9px;color:var(--m);font-size:12px;font-weight:700}.service-line{border-top:1px solid var(--b);padding:12px 0}.service-line label,.service-line .cell{display:grid;gap:6px;font-size:14px;font-weight:700}.service-line strong{text-align:right;padding-top:12px}.remove{border:0;background:transparent;color:var(--m);font-size:25px;cursor:pointer}.mobile-label{display:none}.total{display:flex;justify-content:flex-end;gap:36px;border-top:1px solid var(--b);padding-top:14px;font-size:17px}.attachment{display:flex;align-items:center;justify-content:space-between;gap:16px;border:1px solid var(--b);border-radius:var(--cr);margin-bottom:10px;padding:14px 16px}.attachment.locked{border-color:#9fd6b4;background:#f3fcf6}.attachment>div:first-child{display:grid;gap:4px}.attachment span{color:var(--m);font-size:13px}.attachment-actions{display:flex;gap:8px}.attachment button{border:1px solid var(--b);border-radius:var(--cr);background:var(--s);padding:8px 12px;color:var(--t);font-weight:700;cursor:pointer}.dropzone{position:relative;display:grid;min-height:170px;place-content:center;gap:8px;border:2px dashed color-mix(in srgb,var(--m) 60%,transparent);border-radius:var(--cr);text-align:center;cursor:pointer;transition:border-color .15s,background .15s}.dropzone.drag-active{border-color:var(--a);background:color-mix(in srgb,var(--a) 8%,var(--s))}.dropzone input{position:absolute;inset:0;opacity:0;cursor:pointer}.dropzone span{color:var(--m);font-weight:450}.drop-overlay{position:fixed;inset:0;z-index:60;display:grid;place-content:center;background:color-mix(in srgb,var(--a) 14%,#ffffffd9);pointer-events:none}.drop-overlay div{display:grid;gap:6px;border:2px dashed var(--a);border-radius:var(--r);background:var(--s);padding:34px 44px;text-align:center;box-shadow:0 24px 60px #172b3740}.drop-overlay strong{font-size:19px}.drop-overlay span{color:var(--m)}.error{margin:0 0 14px;border-left:4px solid var(--danger);border-radius:var(--cr);background:color-mix(in srgb,var(--danger) 10%,var(--s));padding:14px;color:var(--danger)}.submit-row{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--b);border-radius:var(--r);background:color-mix(in srgb,var(--s) 96%,transparent);box-shadow:0 -8px 24px #172b3718;padding:14px 18px}.submit-row>span{color:#43835f}.submit-row button{min-width:190px;border:0;border-radius:var(--cr);background:var(--a);color:var(--ac);padding:12px 24px;font:inherit;font-weight:800;cursor:pointer}.submit-row button:disabled{opacity:.6;cursor:wait}@media(max-width:760px){fieldset{padding:18px 16px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.intro{display:grid;gap:10px}.line-head{display:none}.service-line{grid-template-columns:1fr 1fr}.service-line>*{grid-column:1/-1}.service-line label:nth-child(3){grid-column:1}.service-line>strong{grid-column:2;grid-row:3;text-align:left}.service-line>.remove{grid-column:1/-1}.mobile-label{display:block;color:var(--m);font-size:12px}.attachment{align-items:flex-start}.attachment-actions{align-items:center}.submit-row>span{display:none}.submit-row button{width:100%}}\n"] }]
|
|
1948
|
-
}], propDecorators: { initialBill: [{
|
|
1949
|
-
type: Input
|
|
1950
|
-
}], attachments: [{
|
|
1951
|
-
type: Input
|
|
1952
|
-
}], appearance: [{
|
|
1953
|
-
type: Input
|
|
1954
|
-
}], sessionEndpoint: [{
|
|
1955
|
-
type: Input
|
|
1956
|
-
}], getSession: [{
|
|
2108
|
+
}
|
|
2109
|
+
</section>
|
|
2110
|
+
}
|
|
2111
|
+
<section class="card grand" [attr.aria-label]="grandTotalLabel">
|
|
2112
|
+
<div class="grid row totals">
|
|
2113
|
+
<span class="rowlabel grand-label">{{ grandTotalLabel }}</span>
|
|
2114
|
+
@for (bucket of buckets; track bucket.id; let bucketIndex = $index) {
|
|
2115
|
+
<span class="count">{{ data?.grandTotals?.[bucketIndex] ?? 0 }}</span>
|
|
2116
|
+
}
|
|
2117
|
+
<span class="total">{{ data?.grandTotal ?? 0 }}</span>
|
|
2118
|
+
</div>
|
|
2119
|
+
</section>
|
|
2120
|
+
</div>
|
|
2121
|
+
`, styles: [":host{display:block}.mbtk{color:var(--t);font-family:var(--font);font-size:14px}.heading{margin-bottom:14px}.heading h2{margin:0;font-size:24px}.card{margin-bottom:16px;border:1.5px solid var(--b);border-radius:var(--r);background:var(--s);overflow:hidden}.grid{display:grid;grid-template-columns:var(--cols);gap:0 10px;align-items:center;padding:0 16px}.head{padding:12px 16px;border-bottom:1px solid var(--b)}.title{display:flex;align-items:center;gap:9px;min-width:0}.dot{flex:0 0 auto;width:11px;height:11px;border-radius:50%}.title-text{min-width:0}.title-text strong{display:block;font-size:15.5px;line-height:1.3}.basis{display:block;color:var(--m);font-size:12px;line-height:1.35}.pill{justify-self:stretch;padding:5px 6px;border-radius:999px;font-size:11px;font-weight:750;line-height:1.25;text-align:center;white-space:nowrap}.colhead{justify-self:stretch;color:var(--m);font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;text-align:center;white-space:nowrap}.row{border-top:1px solid color-mix(in srgb,var(--b) 60%,transparent);min-height:44px}.row:first-of-type{border-top:0}.rowlabel{color:var(--m);font-size:13px;line-height:1.35;padding:9px 0;overflow-wrap:anywhere}.count,.total{text-align:center;font-variant-numeric:tabular-nums}.zero{color:var(--m)}.link{border:0;background:none;padding:6px 8px;color:var(--a);font:inherit;font-weight:750;font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px}.link:hover{text-decoration:underline;background:color-mix(in srgb,var(--a) 8%,transparent)}.totals{background:color-mix(in srgb,var(--bg) 78%,var(--s));border-top:1px solid var(--b);font-weight:760}.totals .rowlabel{color:var(--t);font-weight:760}.empty{margin:0;padding:22px 16px;color:var(--m);text-align:center}.grand{border-color:var(--b)}.grand .row{min-height:52px}.grand-label{font-size:14px;font-weight:800}@media(max-width:760px){.pill,.colhead,.count{display:none}.grid{grid-template-columns:minmax(0,1fr) auto}}\n"] }]
|
|
2122
|
+
}], propDecorators: { data: [{
|
|
1957
2123
|
type: Input
|
|
1958
|
-
}],
|
|
2124
|
+
}], buckets: [{
|
|
1959
2125
|
type: Input
|
|
1960
2126
|
}], heading: [{
|
|
1961
2127
|
type: Input
|
|
1962
|
-
}],
|
|
1963
|
-
type: Input
|
|
1964
|
-
}], submitLabel: [{
|
|
1965
|
-
type: Input
|
|
1966
|
-
}], procedureOptions: [{
|
|
1967
|
-
type: Input
|
|
1968
|
-
}], modifierOptions: [{
|
|
2128
|
+
}], grandTotalLabel: [{
|
|
1969
2129
|
type: Input
|
|
1970
|
-
}],
|
|
2130
|
+
}], emptyLabel: [{
|
|
1971
2131
|
type: Input
|
|
1972
|
-
}],
|
|
2132
|
+
}], appearance: [{
|
|
1973
2133
|
type: Input
|
|
1974
|
-
}],
|
|
1975
|
-
type: Output
|
|
1976
|
-
}], submissionError: [{
|
|
1977
|
-
type: Output
|
|
1978
|
-
}], billChange: [{
|
|
2134
|
+
}], cellSelected: [{
|
|
1979
2135
|
type: Output
|
|
1980
|
-
}], onWindowDragOver: [{
|
|
1981
|
-
type: HostListener,
|
|
1982
|
-
args: ["window:dragover", ["$event"]]
|
|
1983
|
-
}], onWindowDragLeave: [{
|
|
1984
|
-
type: HostListener,
|
|
1985
|
-
args: ["window:dragleave", ["$event"]]
|
|
1986
|
-
}], onWindowDrop: [{
|
|
1987
|
-
type: HostListener,
|
|
1988
|
-
args: ["window:drop", ["$event"]]
|
|
1989
2136
|
}] } });
|
|
1990
2137
|
|
|
1991
2138
|
const THEME$1 = {
|