@mindbill/angular 0.11.2 → 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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Input, Component, EventEmitter, effect, Output, Directive, inject, ChangeDetectorRef, HostListener } from '@angular/core';
3
- import { createBillLifecycleClient, createBillReferenceClient, createBillSubmissionClient, createOrganizationClient } from '@mindbill/browser';
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
- class MindBillBillLifecycleComponent {
192
- billId = "";
193
- sessionEndpoint = "/api/mindbill/session";
194
- apiBaseUrl = "https://app.mindbill.org";
195
- getSession;
196
- refreshInterval = 60_000;
197
- appearance = { preset: "mindbill" };
198
- billingError = new EventEmitter();
199
- store = new MindBillLifecycleStore();
200
- lifecycleStages = [
201
- { id: "submitted", label: "Submitted" }, { id: "accepted", label: "Accepted" },
202
- { id: "response", label: "Payer response" }, { id: "follow_up", label: "Follow-up" },
203
- { id: "closed", label: "Closed" },
204
- ];
205
- panel = "";
206
- notice = "";
207
- closeReason = "";
208
- payment = { amount: 0, method: "check", depositDate: new Date().toISOString().slice(0, 10) };
209
- review = { reason: "", payerClaimControlNumber: "", disputedAmount: 0 };
210
- constructor() { effect(() => { const error = this.store.error(); if (error)
211
- this.billingError.emit(error); }); }
212
- ngOnChanges(changes) {
213
- if (changes["billId"] || changes["sessionEndpoint"] || changes["apiBaseUrl"] || changes["getSession"]) {
214
- if (!this.billId.trim()) {
215
- this.store.disconnect();
216
- return;
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
- this.store.connect({ billId: this.billId, sessionEndpoint: this.sessionEndpoint, apiBaseUrl: this.apiBaseUrl, ...(this.getSession ? { getSession: this.getSession } : {}) }, this.refreshInterval);
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
- ngOnDestroy() { this.store.disconnect(); }
222
- get themeStyle() {
223
- return mindBillAngularAppearanceStyle(this.appearance);
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
- stateLabel(data) { return data.lifecycle.state.replace(/_/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
226
- lifecycleDetail(data) { return data.lifecycle.submittedAt ? `Submitted ${new Date(data.lifecycle.submittedAt).toLocaleDateString()}${data.lifecycle.agingDays != null ? ` · ${data.lifecycle.agingDays} days old` : ""}` : "Submitted bill"; }
227
- currentStageIndex(data) { const value = data.lifecycle.state.toLowerCase(); if (value.includes("closed") || value.includes("written_off"))
228
- return 4; if (value.includes("second_review") || value.includes("ibr") || value.includes("lien") || value.includes("appeal"))
229
- return 3; if (value.includes("processed") || value.includes("paid") || value.includes("denied") || value.includes("rejected") || value.includes("response"))
230
- return 2; if (value.includes("accepted"))
231
- return 1; return 0; }
232
- activityLabel(type) { return type.replace(/[._-]+/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()); }
233
- historyExpanded = new Set();
234
- toggleHistory(id) { if (this.historyExpanded.has(id))
235
- this.historyExpanded.delete(id);
236
- else
237
- this.historyExpanded.add(id); }
238
- isHistoryOpen(id) { return this.historyExpanded.has(id); }
239
- 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" }); }
240
- 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."; }
241
- beginAction(action, data) { if (action === "post_payment")
242
- this.panel = "payment";
243
- else if (action === "second_review" || action === "independent_bill_review")
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 &amp; 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: MindBillBillLifecycleComponent, decorators: [{
434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
346
435
  type: Component,
347
- args: [{ selector: "mindbill-bill-lifecycle", standalone: true, imports: [CommonModule, FormsModule, MindBillBillRejectionNoticeComponent], template: `
348
- <section class="mb" [ngStyle]="themeStyle">
349
- @if (!billId) {
350
- <div class="state error"><strong>A submitted bill ID is required.</strong></div>
351
- } @else if (store.loading() && !store.data()) {
352
- <div class="state">Loading billing…</div>
353
- } @else if (store.error() && !store.data()) {
354
- <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>
355
- } @else if (store.data(); as data) {
356
- <header class="summary">
357
- <div><span class="eyebrow">Bill {{ data.bill.billNumber }}</span><h2>{{ stateLabel(data) }}</h2><p>{{ lifecycleDetail(data) }}</p></div>
358
- <div class="money"><span>{{ data.bill.balanceDue | currency }}</span><small>balance</small></div>
359
- </header>
360
-
361
- @if (data.lifecycle.state.toLowerCase() === 'rejected' && data.rejection) {
362
- <mindbill-bill-rejection-notice
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 &amp; 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
- }], refreshInterval: [{
453
+ }], disabled: [{
443
454
  type: Input
444
- }], appearance: [{
455
+ }], ariaLabel: [{
445
456
  type: Input
446
- }], billingError: [{
457
+ }], valueChange: [{
447
458
  type: Output
448
459
  }] } });
449
460
 
450
- const EMPTY_PROCEDURE_LINE = {
451
- code: "",
452
- modifiers: [],
453
- units: 1,
454
- charge: 0,
455
- };
456
- function isEmptyProcedureLine(line) {
457
- return !line.code.trim()
458
- && line.modifiers.length === 0
459
- && line.units === 1
460
- && !line.serviceDate
461
- && !line.serviceDateEnd
462
- && !(line.diagnosisPointers?.length)
463
- && !line.charge;
464
- }
465
- /** Keeps completed/partial lines plus one keyboard-ready empty row. */
466
- function ensureTrailingProcedureLine(lines) {
467
- const entered = lines
468
- .filter((line) => !isEmptyProcedureLine(line))
469
- .map((line) => ({ ...line, modifiers: [...line.modifiers] }));
470
- return entered.length < 50
471
- ? [...entered, { ...EMPTY_PROCEDURE_LINE, modifiers: [] }]
472
- : entered;
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: "#f5f7fa", s: "#fff", t: "#1f2d3d", m: "#66788a", b: "#d9e2ec", r: "8px", cr: "6px", font: "Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif" },
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 mindBillAgingDays(bill, now = new Date()) {
482
- if (bill.agingDays != null)
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 buildMindBillReportRows(bills, dimension) {
529
- const rows = new Map();
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 buildMindBillReportCsv(rows) {
543
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
544
- 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");
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 DashboardBase {
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,1239 +623,1516 @@ class DashboardBase {
555
623
  "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
556
624
  };
557
625
  }
558
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
559
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: DashboardBase, isStandalone: true, inputs: { appearance: "appearance" }, ngImport: i0 });
560
- }
561
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DashboardBase, decorators: [{
562
- type: Directive
563
- }], propDecorators: { appearance: [{
564
- type: Input
565
- }] } });
566
- const sharedStyles = `
567
- :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)}
568
- .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}
569
- `;
570
- class MindBillBillAgingSummaryComponent extends DashboardBase {
571
- buckets = [];
572
- selectedBucket = "";
573
- bucketSelected = new EventEmitter();
574
- width(balance) { const max = Math.max(1, ...this.buckets.map((item) => item.balance)); return Math.max(balance ? 8 : 0, balance / max * 100); }
575
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
576
- 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" }] });
577
- }
578
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillAgingSummaryComponent, decorators: [{
579
- type: Component,
580
- 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"] }]
581
- }], propDecorators: { buckets: [{
582
- type: Input
583
- }], selectedBucket: [{
584
- type: Input
585
- }], bucketSelected: [{
586
- type: Output
587
- }] } });
588
- class MindBillBillListComponent extends DashboardBase {
589
- bills = [];
590
- billSelected = new EventEmitter();
591
- age(bill) { return mindBillAgingDays(bill); }
592
- label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
593
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
594
- 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" }] });
595
- }
596
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillListComponent, decorators: [{
597
- type: Component,
598
- 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"] }]
599
- }], propDecorators: { bills: [{
600
- type: Input
601
- }], billSelected: [{
602
- type: Output
603
- }] } });
604
- class MindBillBillingDashboardComponent extends DashboardBase {
605
- bills = [];
606
- heading = "Billing dashboard";
607
- description = "Track every submitted bill from payer acceptance through payment.";
608
- billSelected = new EventEmitter();
609
- createBill = new EventEmitter();
610
- query = "";
611
- state = "";
612
- agingBucket = "";
613
- get summary() { return summarizeMindBillDashboard(this.bills); }
614
- get states() { return [...new Set(this.bills.map((bill) => bill.state))].sort(); }
615
- 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)))); }
616
- get agingLabel() { return this.summary.aging.find((item) => item.id === this.agingBucket)?.label ?? ""; }
617
- toggleAging(bucket) { this.agingBucket = this.agingBucket === bucket ? "" : bucket; }
618
- label(value) { return value.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
619
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
620
- 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" }] });
621
- }
622
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
623
- type: Component,
624
- 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"] }]
625
- }], propDecorators: { bills: [{
626
- type: Input
627
- }], heading: [{
628
- type: Input
629
- }], description: [{
630
- type: Input
631
- }], billSelected: [{
632
- type: Output
633
- }], createBill: [{
634
- type: Output
635
- }] } });
636
- class MindBillBillingReportComponent extends DashboardBase {
637
- bills = [];
638
- heading = "Billing report";
639
- dimension = "status";
640
- get rows() { return buildMindBillReportRows(this.bills, this.dimension); }
641
- 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); }
642
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
643
- 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" }] });
644
- }
645
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingReportComponent, decorators: [{
646
- type: Component,
647
- 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"] }]
648
- }], propDecorators: { bills: [{
649
- type: Input
650
- }], heading: [{
651
- type: Input
652
- }] } });
653
-
654
- const MINDBILL_STATUS_AGING_BUCKETS = [
655
- { id: "current", label: "0–30 days" },
656
- { id: "31-60", label: "31–60 days" },
657
- { id: "61-90", label: "61–90 days" },
658
- { id: "91+", label: "91+ days" },
659
- ];
660
- const DEFAULT_STATE_ORDER = [
661
- "draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
662
- "paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
663
- ];
664
- const agingDays = (bill, now) => {
665
- if (bill.agingDays != null)
666
- return Math.max(0, Math.floor(bill.agingDays));
667
- const source = bill.submittedAt ?? bill.updatedAt;
668
- if (!source)
669
- return 0;
670
- const time = new Date(source).getTime();
671
- return Number.isFinite(time) ? Math.max(0, Math.floor((now.getTime() - time) / 86_400_000)) : 0;
672
- };
673
- const agingBucketIndex = (days) => (days <= 30 ? 0 : days <= 60 ? 1 : days <= 90 ? 2 : 3);
674
- const stateLabel = (state) => state.replace(/_/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
675
- const emptyCell = (state, bucket) => ({ state, bucket, count: 0, balance: 0, bills: [] });
676
- function buildMindBillStatusAgingMatrix(bills, stateOrder = DEFAULT_STATE_ORDER, now = new Date()) {
677
- const orderIndex = new Map(stateOrder.map((state, index) => [state.toLowerCase(), index]));
678
- const states = [...new Set(bills.map((bill) => bill.state))].sort((a, b) => {
679
- const left = orderIndex.get(a.toLowerCase()) ?? stateOrder.length;
680
- const right = orderIndex.get(b.toLowerCase()) ?? stateOrder.length;
681
- return left - right || a.localeCompare(b);
682
- });
683
- const columnTotals = MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell("all", bucket.id));
684
- const grandTotal = emptyCell("all", "total");
685
- const rows = states.map((state) => ({
686
- state,
687
- label: stateLabel(state),
688
- cells: MINDBILL_STATUS_AGING_BUCKETS.map((bucket) => emptyCell(state, bucket.id)),
689
- total: emptyCell(state, "total"),
690
- }));
691
- const rowByState = new Map(rows.map((row) => [row.state, row]));
692
- for (const bill of bills) {
693
- const row = rowByState.get(bill.state);
694
- const bucketIndex = agingBucketIndex(agingDays(bill, now));
695
- for (const cell of [row.cells[bucketIndex], row.total, columnTotals[bucketIndex], grandTotal]) {
696
- cell.count += 1;
697
- cell.balance += bill.balanceDue;
698
- cell.bills.push(bill);
699
- }
626
+ get procedureComboOptions() {
627
+ return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
700
628
  }
701
- return { rows, columnTotals, grandTotal };
702
- }
703
- function buildMindBillStatusAgingCsv(bills, stateOrder) {
704
- const matrix = buildMindBillStatusAgingMatrix(bills, stateOrder);
705
- const quote = (value) => `"${String(value).replace(/"/g, '""')}"`;
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);
629
+ get taxonomyComboOptions() {
630
+ return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
729
631
  }
730
- get themeStyle() {
731
- const base = THEME$3[this.appearance.preset ?? "mindbill"] ?? THEME$3["mindbill"];
732
- return {
733
- "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
734
- "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
735
- "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
736
- "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
737
- "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
738
- };
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 }));
739
636
  }
740
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillStatusAgingMatrixComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
741
- 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: `
742
- <section class="mbmx" [ngStyle]="themeStyle" aria-label="Bills by status and age">
743
- <header><h2>{{ heading }}</h2><p>{{ description }}</p></header>
744
- <div class="card">
745
- <table>
746
- <thead>
747
- <tr><th class="status">Status</th>@for (bucket of buckets; track bucket.id) { <th>{{ bucket.label }}</th> }<th>Total</th></tr>
748
- </thead>
749
- <tbody>
750
- @for (row of matrix.rows; track row.state) {
751
- <tr>
752
- <td class="status"><i class="pill">{{ row.label }}</i></td>
753
- @for (cell of row.cells; track cell.bucket) {
754
- <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>
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);
755
667
  }
756
- <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>
757
- </tr>
758
- }
759
- <tr class="totals">
760
- <td class="status">Total</td>
761
- @for (cell of matrix.columnTotals; track cell.bucket) {
762
- <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>
763
- }
764
- <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>
765
- </tr>
766
- </tbody>
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>
668
+ else {
669
+ this.postalStatus = "ZIP not found";
788
670
  }
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
671
  }
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
- /** Date helpers shared by the Angular submission surfaces. Mirrors the React
821
- * package's parseBillSubmissionDate/formatBillSubmissionDate exactly so both
822
- * frameworks accept and render the same values. */
823
- function parseMindBillSubmissionDate(value) {
824
- const trimmed = value.trim();
825
- const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
826
- const us = /^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$/.exec(trimmed);
827
- const digits = /^(\d{2})(\d{2})(\d{4})$/.exec(trimmed.replace(/\D/g, ""));
828
- const match = iso ? [iso[2], iso[3], iso[1]] : us ? [us[1], us[2], us[3]] : digits ? [digits[1], digits[2], digits[3]] : null;
829
- if (!match)
830
- return undefined;
831
- const month = Number(match[0]);
832
- const day = Number(match[1]);
833
- const year = Number(match[2]);
834
- const date = new Date(Date.UTC(year, month - 1, day));
835
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day)
836
- return undefined;
837
- return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
838
- }
839
- function formatMindBillSubmissionDate(value) {
840
- const parsed = value ? parseMindBillSubmissionDate(value) : undefined;
841
- if (!parsed)
842
- return "";
843
- const [year, month, day] = parsed.split("-");
844
- return `${month}/${day}/${year}`;
845
- }
846
- /** Accepts complete CPT (5 digits), HCPCS (letter + 4 digits), or California
847
- * medical-legal codes typed directly into the procedure picker. */
848
- function mindBillCustomProcedureOption(query) {
849
- const code = query.trim().toUpperCase().replace(/\s+/g, "");
850
- if (!/^(?:\d{5}|[A-Z]\d{4}|ML(?:20[0-5]|PRR))$/.test(code))
851
- return null;
852
- return { id: code, label: code, detail: "Use this CPT, HCPCS, or medical-legal code" };
853
- }
854
-
855
- /** Styled searchable dropdown shared by the submission form's procedure,
856
- * modifier, taxonomy, payer, and diagnosis pickers. Mirrors the React
857
- * package's ComboBox: query filtering, optional custom-code creation,
858
- * loading states, and infinite scroll via (endReached). */
859
- class MindBillComboBoxComponent {
860
- value = "";
861
- placeholder = "";
862
- options = [];
863
- disabled = false;
864
- loading = false;
865
- loadingMore = false;
866
- invalid = false;
867
- preserveValueOnOpen = false;
868
- ariaLabel = "";
869
- createOption;
870
- opened = new EventEmitter();
871
- queryChange = new EventEmitter();
872
- endReached = new EventEmitter();
873
- selected = new EventEmitter();
874
- open = false;
875
- query = "";
876
- closeTimer;
877
- get visible() {
878
- const q = this.query.trim().toLowerCase();
879
- const matches = q
880
- ? this.options.filter((option) => `${option.id} ${option.label} ${option.detail ?? ""}`.toLowerCase().includes(q))
881
- : this.options;
882
- const custom = this.createOption?.(this.query) ?? null;
883
- return custom && !matches.some((option) => option.id.toUpperCase() === custom.id.toUpperCase())
884
- ? [...matches, custom]
885
- : matches;
672
+ catch {
673
+ this.postalStatus = "ZIP lookup unavailable";
674
+ }
675
+ this.changeDetector.markForCheck();
676
+ }, 180);
886
677
  }
887
- get showMenu() {
888
- return this.open && (this.loading || this.visible.length > 0 || this.query.trim().length >= 2);
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);
889
693
  }
890
- onFocus() {
891
- clearTimeout(this.closeTimer);
892
- const next = this.preserveValueOnOpen ? this.value : "";
893
- this.open = true;
894
- this.query = next;
895
- this.opened.emit();
896
- if (next)
897
- this.queryChange.emit(next);
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);
898
700
  }
899
- onInput(event) {
900
- this.open = true;
901
- this.query = event.target.value;
902
- this.queryChange.emit(this.query);
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);
903
725
  }
904
- onBlur() {
905
- this.closeTimer = setTimeout(() => { this.open = false; }, 120);
726
+ diagnosisScrolled(event) {
727
+ const element = event.currentTarget;
728
+ if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
729
+ this.searchDiagnoses(false);
906
730
  }
907
- onScroll(event) {
908
- const menu = event.currentTarget;
909
- if (menu.scrollHeight - menu.scrollTop - menu.clientHeight < 120)
910
- this.endReached.emit();
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);
911
737
  }
912
- choose(option) {
913
- this.selected.emit(option);
914
- this.open = false;
915
- this.query = "";
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);
916
746
  }
917
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
918
- 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: `
919
- <div class="combo">
920
- <input
921
- type="text"
922
- role="combobox"
923
- autocomplete="off"
924
- [attr.aria-label]="ariaLabel"
925
- [attr.aria-invalid]="invalid || null"
926
- [attr.aria-expanded]="showMenu"
927
- [disabled]="disabled"
928
- [placeholder]="placeholder"
929
- [value]="open ? query : value"
930
- (focus)="onFocus()"
931
- (click)="onFocus()"
932
- (input)="onInput($event)"
933
- (blur)="onBlur()"
934
- />
935
- @if (showMenu) {
936
- <div class="menu" role="listbox" (scroll)="onScroll($event)">
937
- @if (loading && !visible.length) { <div class="status">Loading…</div> }
938
- @else if (visible.length) {
939
- @for (option of visible; track option.id) {
940
- <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
941
- <strong>{{ option.label }}</strong>
942
- @if (option.detail) { <small>{{ option.detail }}</small> }
943
- </button>
944
- }
945
- } @else { <div class="status">No matches</div> }
946
- @if (loadingMore) { <div class="status">Loading more…</div> }
947
- </div>
948
- }
949
- </div>
950
- `, 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 }] });
951
- }
952
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillComboBoxComponent, decorators: [{
953
- type: Component,
954
- args: [{ selector: "mindbill-combo-box", standalone: true, imports: [CommonModule], template: `
955
- <div class="combo">
956
- <input
957
- type="text"
958
- role="combobox"
959
- autocomplete="off"
960
- [attr.aria-label]="ariaLabel"
961
- [attr.aria-invalid]="invalid || null"
962
- [attr.aria-expanded]="showMenu"
963
- [disabled]="disabled"
964
- [placeholder]="placeholder"
965
- [value]="open ? query : value"
966
- (focus)="onFocus()"
967
- (click)="onFocus()"
968
- (input)="onInput($event)"
969
- (blur)="onBlur()"
970
- />
971
- @if (showMenu) {
972
- <div class="menu" role="listbox" (scroll)="onScroll($event)">
973
- @if (loading && !visible.length) { <div class="status">Loading…</div> }
974
- @else if (visible.length) {
975
- @for (option of visible; track option.id) {
976
- <button type="button" role="option" (mousedown)="$event.preventDefault()" (click)="choose(option)">
977
- <strong>{{ option.label }}</strong>
978
- @if (option.detail) { <small>{{ option.detail }}</small> }
979
- </button>
980
- }
981
- } @else { <div class="status">No matches</div> }
982
- @if (loadingMore) { <div class="status">Loading more…</div> }
983
- </div>
984
- }
985
- </div>
986
- `, 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"] }]
987
- }], propDecorators: { value: [{
988
- type: Input
989
- }], placeholder: [{
990
- type: Input
991
- }], options: [{
992
- type: Input
993
- }], disabled: [{
994
- type: Input
995
- }], loading: [{
996
- type: Input
997
- }], loadingMore: [{
998
- type: Input
999
- }], invalid: [{
1000
- type: Input
1001
- }], preserveValueOnOpen: [{
1002
- type: Input
1003
- }], ariaLabel: [{
1004
- type: Input
1005
- }], createOption: [{
1006
- type: Input
1007
- }], opened: [{
1008
- type: Output
1009
- }], queryChange: [{
1010
- type: Output
1011
- }], endReached: [{
1012
- type: Output
1013
- }], selected: [{
1014
- type: Output
1015
- }] } });
1016
- /** MM/DD/YYYY masked date input. Emits only parseable ISO dates (or "" when
1017
- * cleared) and reformats the display once a complete date round-trips, exactly
1018
- * like the React package's TextDateInput. */
1019
- class MindBillDateInputComponent {
1020
- value;
1021
- disabled = false;
1022
- ariaLabel = "";
1023
- valueChange = new EventEmitter();
1024
- display = "";
1025
- invalid = false;
1026
- ngOnChanges(changes) {
1027
- if (changes["value"]) {
1028
- this.display = formatMindBillSubmissionDate(this.value);
1029
- this.invalid = false;
1030
- }
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();
1031
753
  }
1032
- onInput(event) {
1033
- const next = event.target.value;
1034
- const parsed = parseMindBillSubmissionDate(next);
1035
- this.display = next;
1036
- this.invalid = Boolean(next && !parsed);
1037
- if (!next.trim())
1038
- this.valueChange.emit("");
1039
- else if (parsed)
1040
- this.valueChange.emit(parsed);
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);
1041
758
  }
1042
- onBlur() {
1043
- this.invalid = Boolean(this.display && !parseMindBillSubmissionDate(this.display));
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);
1044
763
  }
1045
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1046
- 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: `
1047
- <input
1048
- type="text"
1049
- inputmode="numeric"
1050
- autocomplete="off"
1051
- placeholder="MM/DD/YYYY"
1052
- [attr.aria-label]="ariaLabel"
1053
- [attr.aria-invalid]="invalid || null"
1054
- [disabled]="disabled"
1055
- [value]="display"
1056
- (input)="onInput($event)"
1057
- (blur)="onBlur()"
1058
- />
1059
- @if (invalid) { <small>Use MM/DD/YYYY</small> }
1060
- `, 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 }] });
1061
- }
1062
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillDateInputComponent, decorators: [{
1063
- type: Component,
1064
- args: [{ selector: "mindbill-date-input", standalone: true, imports: [CommonModule], template: `
1065
- <input
1066
- type="text"
1067
- inputmode="numeric"
1068
- autocomplete="off"
1069
- placeholder="MM/DD/YYYY"
1070
- [attr.aria-label]="ariaLabel"
1071
- [attr.aria-invalid]="invalid || null"
1072
- [disabled]="disabled"
1073
- [value]="display"
1074
- (input)="onInput($event)"
1075
- (blur)="onBlur()"
1076
- />
1077
- @if (invalid) { <small>Use MM/DD/YYYY</small> }
1078
- `, 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"] }]
1079
- }], propDecorators: { value: [{
1080
- type: Input
1081
- }], disabled: [{
1082
- type: Input
1083
- }], ariaLabel: [{
1084
- type: Input
1085
- }], valueChange: [{
1086
- type: Output
1087
- }] } });
1088
-
1089
- const MAX_PDF_BYTES = 25 * 1024 * 1024;
1090
- const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
1091
- const MAX_DOCUMENTS = 20;
1092
- const PROCEDURES = [
1093
- { code: "ML200", description: "Missed appointment for a medical-legal evaluation", allowedAmount: 503.75 },
1094
- { code: "ML201", description: "Comprehensive medical-legal evaluation", allowedAmount: 2015 },
1095
- { code: "ML202", description: "Follow-up medical-legal evaluation", allowedAmount: 1316.25 },
1096
- { code: "ML203", description: "Supplemental medical-legal evaluation", allowedAmount: 650 },
1097
- { code: "ML204", description: "Medical-legal testimony", allowedAmount: 113.75 },
1098
- { code: "ML205", description: "Review of sub rosa recordings", allowedAmount: 81.25 },
1099
- { code: "MLPRR", description: "Medical-legal record review", allowedAmount: 3 },
1100
- { code: "90791", description: "Psychiatric diagnostic evaluation" },
1101
- { code: "90792", description: "Psychiatric diagnostic evaluation with medical services" },
1102
- { code: "90832", description: "Psychotherapy, 30 minutes" },
1103
- { code: "90834", description: "Psychotherapy, 45 minutes" },
1104
- { code: "90837", description: "Psychotherapy, 60 minutes" },
1105
- { code: "99203", description: "New patient office visit, level 3" },
1106
- { code: "99204", description: "New patient office visit, level 4" },
1107
- { code: "99205", description: "New patient office visit, level 5", allowedAmount: 349.48 },
1108
- { code: "99213", description: "Established patient office visit, level 3" },
1109
- { code: "99214", description: "Established patient office visit, level 4" },
1110
- { code: "99215", description: "Established patient office visit, level 5" },
1111
- { code: "96130", description: "Psychological testing evaluation, first hour" },
1112
- { code: "96131", description: "Psychological testing evaluation, each additional hour", allowedAmount: 131.96 },
1113
- { code: "97161", description: "Physical therapy evaluation, low complexity" },
1114
- { code: "97162", description: "Physical therapy evaluation, moderate complexity" },
1115
- { code: "97163", description: "Physical therapy evaluation, high complexity" },
1116
- { code: "97110", description: "Therapeutic exercises" },
1117
- { code: "97140", description: "Manual therapy techniques" },
1118
- { code: "98940", description: "Chiropractic manipulative treatment, 1–2 regions" },
1119
- { code: "98941", description: "Chiropractic manipulative treatment, 3–4 regions" },
1120
- { code: "72141", description: "MRI cervical spine without contrast" },
1121
- { code: "72148", description: "MRI lumbar spine without contrast" },
1122
- { code: "73721", description: "MRI lower-extremity joint without contrast" },
1123
- { code: "20610", description: "Major joint injection or aspiration" },
1124
- { code: "99070", description: "Supplies and materials" },
1125
- ];
1126
- const MODIFIERS = [
1127
- { code: "92", description: "Primary Treating Physician evaluation" },
1128
- { code: "93", description: "Interpreter required" },
1129
- { code: "94", description: "Agreed Medical Evaluator" },
1130
- { code: "95", description: "Qualified Medical Evaluator" },
1131
- { code: "96", description: "Psychiatric/psychological evaluation" },
1132
- { code: "97", description: "Toxicology evaluation" },
1133
- { code: "98", description: "Oncology evaluation" },
1134
- { code: "59", description: "Distinct procedural service" },
1135
- { code: "XE", description: "Separate encounter" },
1136
- { code: "XS", description: "Separate structure" },
1137
- { code: "XP", description: "Separate practitioner" },
1138
- { code: "XU", description: "Unusual non-overlapping service" },
1139
- ];
1140
- const TAXONOMIES = [
1141
- { code: "103G00000X", description: "Clinical Neuropsychologist" },
1142
- { code: "103T00000X", description: "Psychologist" },
1143
- { code: "111N00000X", description: "Chiropractor" },
1144
- { code: "207Q00000X", description: "Family Medicine" },
1145
- { code: "207R00000X", description: "Internal Medicine" },
1146
- { code: "207X00000X", description: "Orthopaedic Surgery" },
1147
- { code: "207XS0117X", description: "Orthopaedic Surgery of the Spine" },
1148
- { code: "208100000X", description: "Physical Medicine & Rehabilitation" },
1149
- { code: "2083X0100X", description: "Occupational Medicine" },
1150
- { code: "2084N0400X", description: "Neurology" },
1151
- { code: "2084P0800X", description: "Psychiatry" },
1152
- { code: "208VP0000X", description: "Pain Medicine" },
1153
- { code: "213E00000X", description: "Podiatrist" },
1154
- { code: "225100000X", description: "Physical Therapist" },
1155
- { code: "225X00000X", description: "Occupational Therapist" },
1156
- { code: "363A00000X", description: "Physician Assistant" },
1157
- { code: "363L00000X", description: "Nurse Practitioner" },
1158
- ];
1159
- const QUICK_DIAGNOSES = [
1160
- { label: "Psych", code: "Z04.6", description: "General psychiatric examination requested by authority" },
1161
- { label: "Back", code: "M54.50", description: "Low back pain, unspecified" },
1162
- { label: "Neck", code: "M54.2", description: "Cervicalgia" },
1163
- { label: "Left hand", code: "M79.642", description: "Pain in left hand" },
1164
- { label: "Right hand", code: "M79.641", description: "Pain in right hand" },
1165
- { label: "Left knee", code: "M25.562", description: "Pain in left knee" },
1166
- { label: "Right knee", code: "M25.561", description: "Pain in right knee" },
1167
- ];
1168
- const THEME$2 = {
1169
- 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" },
1170
- "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" },
1171
- "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" },
1172
- "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" },
1173
- };
1174
- function deepCopyBill(value) {
1175
- return JSON.parse(JSON.stringify(value));
1176
- }
1177
- function required(value) {
1178
- return typeof value === "string" ? value.trim().length > 0 : value != null;
1179
- }
1180
- async function blobToBase64(blob) {
1181
- const buffer = new Uint8Array(await blob.arrayBuffer());
1182
- let binary = "";
1183
- for (const byte of buffer)
1184
- binary += String.fromCharCode(byte);
1185
- return btoa(binary);
1186
- }
1187
- class MindBillBillSubmissionComponent {
1188
- initialBill;
1189
- attachments = [];
1190
- appearance = { preset: "mindbill" };
1191
- sessionEndpoint = "/api/mindbill/session";
1192
- getSession;
1193
- apiBaseUrl;
1194
- heading = "Bill information";
1195
- description = "Review the bill details, add attachments, and submit.";
1196
- submitLabel = "Submit bill";
1197
- procedureOptions = PROCEDURES;
1198
- modifierOptions = MODIFIERS;
1199
- taxonomyOptions = TAXONOMIES;
1200
- submitter;
1201
- submitted = new EventEmitter();
1202
- submissionError = new EventEmitter();
1203
- billChange = new EventEmitter();
1204
- bill = {};
1205
- workingAttachments = [];
1206
- payerQuery = "";
1207
- payerResults = [];
1208
- payerBusy = false;
1209
- diagnosisQuery = "";
1210
- diagnosisResults = [];
1211
- diagnosisOffset = 0;
1212
- diagnosisBusy = false;
1213
- submitting = false;
1214
- errorMessage = "";
1215
- postalStatus = "";
1216
- dragActive = false;
1217
- invalidFields = new Set();
1218
- quickDiagnoses = QUICK_DIAGNOSES;
1219
- // Zoneless-safe rendering: async work (debounced lookups, fetches, file reads)
1220
- // must schedule change detection explicitly for zone-less Angular hosts.
1221
- changeDetector = inject(ChangeDetectorRef);
1222
- customProcedureOption = mindBillCustomProcedureOption;
1223
- customTaxonomyOption = (query) => {
1224
- const code = query.trim().toUpperCase();
1225
- return /^[A-Z0-9]{10}$/.test(code) ? { id: code, label: code, detail: "Use this taxonomy code" } : null;
1226
- };
1227
- searchTimer;
1228
- postalTimer;
1229
- ngOnChanges(changes) {
1230
- if (changes["initialBill"] && this.initialBill) {
1231
- this.bill = deepCopyBill(this.initialBill);
1232
- if (!this.bill.serviceLines.length)
1233
- this.bill.serviceLines = [{ code: "", units: 1 }];
1234
- this.ensureTrailingLine();
1235
- this.payerQuery = this.bill.claim.claimsAdministrator.name ?? "";
1236
- }
1237
- if (changes["attachments"])
1238
- this.workingAttachments = this.attachments.map((item) => ({ ...item }));
764
+ dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
765
+ hasDiagnosisPointer(line, pointer) {
766
+ return (line.diagnosisPointers ?? []).includes(pointer);
1239
767
  }
1240
- get themeStyle() {
1241
- const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
1242
- return {
1243
- "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
1244
- "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
1245
- "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
1246
- "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
1247
- "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
1248
- };
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);
1249
774
  }
1250
- get procedureComboOptions() {
1251
- return this.procedureOptions.map((item) => ({ id: item.code, label: item.code, detail: item.description }));
775
+ setAuthorizationNumber(value) {
776
+ this.bill.service.authorizationNumber = value.trim() ? value : null;
777
+ this.billChange.emit(this.bill);
1252
778
  }
1253
- get taxonomyComboOptions() {
1254
- return this.taxonomyOptions.map((item) => ({ id: item.code, label: item.description, detail: item.code }));
1255
- }
1256
- modifierComboOptions(line) {
1257
- return this.modifierOptions
1258
- .filter((item) => !(line.modifiers ?? []).includes(item.code))
1259
- .map((item) => ({ id: item.code, label: `−${item.code}`, detail: item.description }));
1260
- }
1261
- referenceClient() {
1262
- return createBillReferenceClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) });
1263
- }
1264
- setDate(path, value) {
1265
- if (path === "patient.dateOfBirth")
1266
- this.bill.patient.dateOfBirth = value;
1267
- else if (path === "service.date")
1268
- this.bill.service.date = value;
1269
- else
1270
- this.bill.claim.dateOfInjury = value;
1271
- if (value)
1272
- this.invalidFields.delete(path);
779
+ selectTaxonomy(option) {
780
+ this.bill.renderingProvider.taxonomy = option.id;
781
+ this.invalidFields.delete("renderingProvider.taxonomy");
1273
782
  this.billChange.emit(this.bill);
1274
783
  }
1275
- postalChanged(value) {
1276
- clearTimeout(this.postalTimer);
1277
- this.postalStatus = "";
1278
- if (!/^\d{5}$/.test(value.trim()))
1279
- return;
1280
- this.postalStatus = "Looking up ZIP…";
1281
- this.postalTimer = setTimeout(async () => {
1282
- try {
1283
- const place = await this.referenceClient().lookupPostalCode(value);
1284
- if (place) {
1285
- this.bill.patient.address.city = place.city;
1286
- this.bill.patient.address.state = place.state.toUpperCase();
1287
- this.postalStatus = `${place.city}, ${place.state.toUpperCase()} filled from ZIP`;
1288
- this.invalidFields.delete("patient.address.city");
1289
- this.invalidFields.delete("patient.address.state");
1290
- this.billChange.emit(this.bill);
1291
- }
1292
- else {
1293
- this.postalStatus = "ZIP not found";
1294
- }
1295
- }
1296
- catch {
1297
- this.postalStatus = "ZIP lookup unavailable";
1298
- }
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;
1299
791
  this.changeDetector.markForCheck();
1300
- }, 180);
792
+ }
1301
793
  }
1302
- searchPayers() {
1303
- clearTimeout(this.searchTimer);
1304
- this.searchTimer = setTimeout(async () => {
1305
- this.payerBusy = true;
1306
- try {
1307
- this.payerResults = await this.referenceClient().searchClaimsAdministrators(this.payerQuery, this.bill.claim.claimNumber);
1308
- }
1309
- catch {
1310
- this.payerResults = [];
1311
- }
1312
- finally {
1313
- this.payerBusy = false;
1314
- this.changeDetector.markForCheck();
1315
- }
1316
- }, 180);
794
+ onWindowDragLeave(event) {
795
+ if (!event.relatedTarget) {
796
+ this.dragActive = false;
797
+ this.changeDetector.markForCheck();
798
+ }
1317
799
  }
1318
- selectPayer(payer) {
1319
- this.bill.claim.claimsAdministrator = { id: payer.id, name: payer.name };
1320
- this.payerQuery = payer.name;
1321
- this.payerResults = [];
1322
- this.invalidFields.delete("claim.claimsAdministrator");
1323
- this.billChange.emit(this.bill);
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
+ }
1324
807
  }
1325
- searchDiagnoses(reset) {
1326
- if (this.diagnosisBusy)
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.`;
1327
816
  return;
1328
- if (reset) {
1329
- this.diagnosisOffset = 0;
1330
- this.diagnosisResults = [];
1331
817
  }
1332
- clearTimeout(this.searchTimer);
1333
- this.searchTimer = setTimeout(async () => {
1334
- this.diagnosisBusy = true;
1335
- try {
1336
- const results = await this.referenceClient().searchDiagnosisCodes(this.diagnosisQuery, 60, this.diagnosisOffset);
1337
- this.diagnosisResults = reset ? results : [...this.diagnosisResults, ...results];
1338
- this.diagnosisOffset = this.diagnosisResults.length;
1339
- }
1340
- catch {
1341
- if (reset)
1342
- this.diagnosisResults = [];
1343
- }
1344
- finally {
1345
- this.diagnosisBusy = false;
1346
- this.changeDetector.markForCheck();
1347
- }
1348
- }, reset ? 160 : 0);
1349
- }
1350
- diagnosisScrolled(event) {
1351
- const element = event.currentTarget;
1352
- if (element.scrollTop + element.clientHeight >= element.scrollHeight - 80)
1353
- this.searchDiagnoses(false);
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();
1354
835
  }
1355
- hasDiagnosis(code) { return this.bill.diagnoses.includes(code); }
1356
- toggleDiagnosis(item) {
1357
- if (this.hasDiagnosis(item.code))
1358
- this.removeDiagnosis(item.code);
1359
- else
1360
- this.addDiagnosis(item);
836
+ async filesSelected(event) {
837
+ const input = event.target;
838
+ await this.addFiles(input.files ?? []);
839
+ input.value = "";
1361
840
  }
1362
- addDiagnosis(item) { if (!this.hasDiagnosis(item.code))
1363
- this.bill.diagnoses.push(item.code); this.diagnosisQuery = ""; this.diagnosisResults = []; this.invalidFields.delete("diagnoses"); this.billChange.emit(this.bill); }
1364
- removeDiagnosis(code) { this.bill.diagnoses = this.bill.diagnoses.filter((item) => item !== code); this.billChange.emit(this.bill); }
1365
- ensureTrailingLine() {
1366
- const last = this.bill.serviceLines.at(-1);
1367
- if (!last || last.code.trim())
1368
- this.bill.serviceLines.push({ code: "", units: 1 });
1369
- this.billChange.emit(this.bill);
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");
1370
853
  }
1371
- removeLine(index) { this.bill.serviceLines.splice(index, 1); if (!this.bill.serviceLines.length)
1372
- this.bill.serviceLines.push({ code: "", units: 1 }); this.ensureTrailingLine(); }
1373
- selectProcedure(index, option) {
1374
- this.bill.serviceLines[index].code = option.id;
1375
- this.invalidFields.delete("serviceLines");
1376
- this.ensureTrailingLine();
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;
1377
874
  }
1378
- addModifier(index, option) {
1379
- const line = this.bill.serviceLines[index];
1380
- line.modifiers = [...new Set([...(line.modifiers ?? []), option.id])];
1381
- this.billChange.emit(this.bill);
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;
1382
899
  }
1383
- removeModifier(index, modifier) {
1384
- const line = this.bill.serviceLines[index];
1385
- line.modifiers = (line.modifiers ?? []).filter((item) => item !== modifier);
1386
- this.billChange.emit(this.bill);
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
+ }
1387
932
  }
1388
- dxLetter(pointer) { return String.fromCharCode(64 + pointer); }
1389
- hasDiagnosisPointer(line, pointer) {
1390
- return (line.diagnosisPointers ?? []).includes(pointer);
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 &amp; 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 &amp; 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> }
1002
+ </div>
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>
1012
+ }
1013
+ } @else { <small>Add diagnoses above</small> }
1014
+ </div>
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 &amp; 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 &amp; 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>
1089
+ </div>
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> }
1104
+ </div>
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>
1114
+ }
1115
+ } @else { <small>Add diagnoses above</small> }
1116
+ </div>
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: [{
1136
+ type: Input
1137
+ }], attachments: [{
1138
+ type: Input
1139
+ }], appearance: [{
1140
+ type: Input
1141
+ }], sessionEndpoint: [{
1142
+ type: Input
1143
+ }], getSession: [{
1144
+ type: Input
1145
+ }], apiBaseUrl: [{
1146
+ type: Input
1147
+ }], heading: [{
1148
+ type: Input
1149
+ }], description: [{
1150
+ type: Input
1151
+ }], submitLabel: [{
1152
+ type: Input
1153
+ }], attentionFields: [{
1154
+ type: Input
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: [{
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"]]
1178
+ }] } });
1179
+
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
+ }
1391
1213
  }
1392
- toggleDiagnosisPointer(index, pointer) {
1393
- const line = this.bill.serviceLines[index];
1394
- line.diagnosisPointers = this.hasDiagnosisPointer(line, pointer)
1395
- ? (line.diagnosisPointers ?? []).filter((item) => item !== pointer)
1396
- : [...(line.diagnosisPointers ?? []), pointer].sort((left, right) => left - right);
1397
- this.billChange.emit(this.bill);
1214
+ ngOnDestroy() { this.store.disconnect(); }
1215
+ get themeStyle() {
1216
+ return mindBillAngularAppearanceStyle(this.appearance);
1398
1217
  }
1399
- setAuthorizationNumber(value) {
1400
- this.bill.service.authorizationNumber = value.trim() ? value : null;
1401
- this.billChange.emit(this.bill);
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);
1402
1245
  }
1403
- selectTaxonomy(option) {
1404
- this.bill.renderingProvider.taxonomy = option.id;
1405
- this.invalidFields.delete("renderingProvider.taxonomy");
1406
- this.billChange.emit(this.bill);
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";
1407
1276
  }
1408
- procedureDescription(code) { return this.procedureOptions.find((item) => item.code.toLowerCase() === code.trim().toLowerCase())?.description ?? ""; }
1409
- 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; }
1410
- get totalAllowed() { return this.bill.serviceLines.reduce((sum, line) => sum + (this.allowed(line) ?? 0), 0); }
1411
- onWindowDragOver(event) {
1412
- if (event.dataTransfer?.types.includes("Files")) {
1413
- event.preventDefault();
1414
- this.dragActive = true;
1415
- this.changeDetector.markForCheck();
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 &amp; 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>
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
+ }
1390
+ }
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" }] });
1393
+ }
1394
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillLifecycleComponent, decorators: [{
1395
+ type: Component,
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 &amp; 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>
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>
1416
1506
  }
1507
+ }
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: [{
1511
+ type: Input
1512
+ }], sessionEndpoint: [{
1513
+ type: Input
1514
+ }], apiBaseUrl: [{
1515
+ type: Input
1516
+ }], getSession: [{
1517
+ type: Input
1518
+ }], refreshInterval: [{
1519
+ type: Input
1520
+ }], appearance: [{
1521
+ type: Input
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;
1417
1630
  }
1418
- onWindowDragLeave(event) {
1419
- if (!event.relatedTarget) {
1420
- this.dragActive = false;
1421
- this.changeDetector.markForCheck();
1422
- }
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);
1423
1659
  }
1424
- onWindowDrop(event) {
1425
- if (event.dataTransfer?.files.length) {
1426
- event.preventDefault();
1427
- this.dragActive = false;
1428
- void this.addFiles(event.dataTransfer.files);
1429
- this.changeDetector.markForCheck();
1430
- }
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
+ };
1431
1677
  }
1432
- async addFiles(fileList) {
1433
- const files = Array.from(fileList);
1434
- if (!files.length)
1435
- return;
1436
- this.errorMessage = "";
1437
- const invalid = files.find((file) => file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf"));
1438
- if (invalid) {
1439
- this.errorMessage = `${invalid.name} is not a PDF.`;
1440
- return;
1441
- }
1442
- const oversized = files.find((file) => file.size > MAX_PDF_BYTES);
1443
- if (oversized) {
1444
- this.errorMessage = `${oversized.name} is larger than 25 MB.`;
1445
- return;
1446
- }
1447
- if (this.workingAttachments.length + files.length > MAX_DOCUMENTS) {
1448
- this.errorMessage = `A bill can include at most ${MAX_DOCUMENTS} attachments.`;
1449
- return;
1450
- }
1451
- const existingBytes = this.workingAttachments.reduce((sum, item) => sum + (item.contentBase64 ? item.contentBase64.length * 0.75 : 0), 0);
1452
- if (existingBytes + files.reduce((sum, file) => sum + file.size, 0) > MAX_UPLOAD_BYTES) {
1453
- this.errorMessage = "Attachments exceed the 100 MB upload limit.";
1454
- return;
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: [{
1684
+ type: Input
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: [{
1702
+ type: Input
1703
+ }], selectedBucket: [{
1704
+ type: Input
1705
+ }], bucketSelected: [{
1706
+ type: Output
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: [{
1722
+ type: Output
1723
+ }] } });
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" }] });
1741
+ }
1742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillingDashboardComponent, decorators: [{
1743
+ type: Component,
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: [{
1746
+ type: Input
1747
+ }], heading: [{
1748
+ type: Input
1749
+ }], description: [{
1750
+ type: Input
1751
+ }], billSelected: [{
1752
+ type: Output
1753
+ }], createBill: [{
1754
+ type: Output
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
+ }] } });
1773
+
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" },
1779
+ ];
1780
+ const DEFAULT_STATE_ORDER = [
1781
+ "draft", "incomplete", "created", "sent", "submitted", "accepted", "processed",
1782
+ "paid", "underpaid", "denied", "rejected", "appealing", "second_review", "ibr", "lien", "closed",
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);
1455
1819
  }
1456
- for (const file of files)
1457
- this.workingAttachments.push({ filename: file.name, description: "Supporting document", documentType: "other", contentBase64: await blobToBase64(file), contentUrl: URL.createObjectURL(file) });
1458
- this.changeDetector.markForCheck();
1459
1820
  }
1460
- async filesSelected(event) {
1461
- const input = event.target;
1462
- await this.addFiles(input.files ?? []);
1463
- input.value = "";
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
+
1832
+ const THEME$2 = {
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" },
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" },
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" },
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" },
1837
+ };
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);
1464
1849
  }
1465
- removeAttachment(index) { const item = this.workingAttachments[index]; if (item?.contentUrl?.startsWith("blob:"))
1466
- URL.revokeObjectURL(item.contentUrl); this.workingAttachments.splice(index, 1); }
1467
- previewAttachment(attachment) { if (attachment.contentUrl)
1468
- window.open(attachment.contentUrl, "_blank", "noopener,noreferrer");
1469
- else if (attachment.contentBase64)
1470
- window.open(`data:application/pdf;base64,${attachment.contentBase64}`, "_blank", "noopener,noreferrer"); }
1471
- bad(path) { return this.invalidFields.has(path); }
1472
- validate() {
1473
- const candidates = [
1474
- ["patient.firstName", this.bill.patient.firstName], ["patient.lastName", this.bill.patient.lastName], ["patient.dateOfBirth", parseMindBillSubmissionDate(this.bill.patient.dateOfBirth ?? "")],
1475
- ["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],
1476
- ["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],
1477
- ["claim.claimsAdministrator", this.bill.claim.claimsAdministrator.id], ["diagnoses", this.bill.diagnoses.length],
1478
- ["billingProvider.name", this.bill.billingProvider.name], ["billingProvider.taxId", this.bill.billingProvider.taxId], ["billingProvider.npi", this.bill.billingProvider.npi], ["billingProvider.phone", this.bill.billingProvider.phone],
1479
- ["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],
1480
- ["renderingProvider.name", this.bill.renderingProvider.name], ["renderingProvider.npi", this.bill.renderingProvider.npi], ["renderingProvider.taxonomy", this.bill.renderingProvider.taxonomy],
1481
- ["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],
1482
- ["serviceLines", this.bill.serviceLines.some((line) => required(line.code) && (line.units ?? 1) > 0)],
1483
- ];
1484
- this.invalidFields = new Set(candidates.filter(([, value]) => !value || !required(value)).map(([path]) => path));
1485
- if (this.invalidFields.size) {
1486
- this.errorMessage = `Complete ${this.invalidFields.size} highlighted required field${this.invalidFields.size === 1 ? "" : "s"} before submitting.`;
1487
- setTimeout(() => document.querySelector(".mbs .invalid,.mbs .invalid-set")?.scrollIntoView({ behavior: "smooth", block: "center" }));
1488
- return false;
1489
- }
1490
- return true;
1850
+ get themeStyle() {
1851
+ const base = THEME$2[this.appearance.preset ?? "mindbill"] ?? THEME$2["mindbill"];
1852
+ return {
1853
+ "--a": this.appearance.accentColor ?? base["a"], "--ac": this.appearance.accentTextColor ?? base["ac"],
1854
+ "--bg": this.appearance.backgroundColor ?? base["bg"], "--s": this.appearance.surfaceColor ?? base["s"],
1855
+ "--t": this.appearance.textColor ?? base["t"], "--m": this.appearance.mutedColor ?? base["m"],
1856
+ "--b": this.appearance.borderColor ?? base["b"], "--r": this.appearance.borderRadius ?? base["r"],
1857
+ "--cr": this.appearance.controlRadius ?? base["cr"], "--font": this.appearance.fontFamily ?? base["font"],
1858
+ };
1491
1859
  }
1492
- async submissionDocuments() {
1493
- const documents = [];
1494
- for (const attachment of this.workingAttachments) {
1495
- let contentBase64 = attachment.contentBase64;
1496
- if (!contentBase64 && attachment.contentUrl) {
1497
- const response = await fetch(attachment.contentUrl);
1498
- if (!response.ok)
1499
- throw new Error(`Could not load ${attachment.filename}.`);
1500
- contentBase64 = await blobToBase64(await response.blob());
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>
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>
1501
1878
  }
1502
- if (!contentBase64)
1503
- continue;
1504
- documents.push({
1505
- filename: attachment.filename,
1506
- documentType: attachment.documentType,
1507
- contentBase64,
1508
- ...(attachment.externalId ? { externalId: attachment.externalId } : {}),
1509
- ...(attachment.description ? { description: attachment.description } : {}),
1510
- ...(attachment.reportTypeCode ? { reportTypeCode: attachment.reportTypeCode } : {}),
1511
- });
1512
- }
1513
- return documents;
1514
- }
1515
- async submit(event) {
1516
- event.preventDefault();
1517
- this.errorMessage = "";
1518
- if (!this.validate())
1519
- return;
1520
- this.submitting = true;
1521
- try {
1522
- const bill = deepCopyBill(this.bill);
1523
- bill.patient.dateOfBirth = parseMindBillSubmissionDate(bill.patient.dateOfBirth ?? "") ?? bill.patient.dateOfBirth;
1524
- bill.service.date = parseMindBillSubmissionDate(bill.service.date ?? "") ?? bill.service.date;
1525
- bill.claim.dateOfInjury = parseMindBillSubmissionDate(bill.claim.dateOfInjury ?? "") ?? bill.claim.dateOfInjury;
1526
- bill.serviceLines = bill.serviceLines
1527
- .filter((line) => line.code.trim())
1528
- .map((line) => {
1529
- const charge = line.charge ?? this.allowed(line);
1530
- return charge == null ? line : { ...line, charge };
1531
- });
1532
- const input = { bill, documents: await this.submissionDocuments() };
1533
- const result = this.submitter
1534
- ? await this.submitter(input)
1535
- : await createBillSubmissionClient({ sessionEndpoint: this.sessionEndpoint, ...(this.getSession ? { getSession: this.getSession } : {}), ...(this.apiBaseUrl ? { apiBaseUrl: this.apiBaseUrl } : {}) }).submitBill(input);
1536
- this.submitted.emit(result);
1537
- }
1538
- catch (value) {
1539
- const error = value instanceof Error ? value : new Error("The bill could not be submitted.");
1540
- this.errorMessage = error.message;
1541
- this.submissionError.emit(error);
1542
- }
1543
- finally {
1544
- this.submitting = false;
1545
- this.changeDetector.markForCheck();
1546
- }
1547
- }
1548
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1549
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MindBillBillSubmissionComponent, isStandalone: true, selector: "mindbill-bill-submission", inputs: { initialBill: "initialBill", attachments: "attachments", appearance: "appearance", sessionEndpoint: "sessionEndpoint", getSession: "getSession", apiBaseUrl: "apiBaseUrl", heading: "heading", description: "description", submitLabel: "submitLabel", procedureOptions: "procedureOptions", modifierOptions: "modifierOptions", taxonomyOptions: "taxonomyOptions", submitter: "submitter" }, outputs: { submitted: "submitted", submissionError: "submissionError", billChange: "billChange" }, host: { listeners: { "window:dragover": "onWindowDragOver($event)", "window:dragleave": "onWindowDragLeave($event)", "window:drop": "onWindowDrop($event)" } }, usesOnChanges: true, ngImport: i0, template: `
1550
- <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
1551
- <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
1552
-
1553
- <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
1554
- <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
1555
- <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
1556
- <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>
1557
- <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
1558
- <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
1559
- <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>
1560
- <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
1561
- <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
1562
- </div></fieldset>
1563
-
1564
- <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
1565
- <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>
1566
- <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>
1567
- <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>
1568
- <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
1569
- <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
1570
- <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
1571
- <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
1572
- @if (payerBusy) { <small>Searching directory…</small> }
1573
- @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> }
1574
- @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
1575
- </div>
1576
- <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
1577
- <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
1578
- <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>
1579
- @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> }
1580
- <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1581
- @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
1582
- @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> }
1583
- </div>
1584
- </div></fieldset>
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>
1911
+ }
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
+ }] } });
1585
1939
 
1586
- <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
1587
- <h3 class="wide">Billing provider</h3>
1588
- <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
1589
- <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
1590
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1591
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
1592
- <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
1593
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
1594
- <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
1595
- <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
1596
- <h3 class="wide">Rendering provider</h3>
1597
- <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
1598
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1599
- <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1600
- <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)"/>
1601
- <small>Search by specialty name or 10-character taxonomy code.</small>
1602
- </div>
1603
- <h3 class="wide">Place of service</h3>
1604
- <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>
1605
- <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
1606
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
1607
- <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
1608
- <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
1609
- </div></fieldset>
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
+ };
1965
+ }
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
+ }
1610
1974
 
1611
- <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1612
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1613
- @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1614
- <div class="cell"><span class="mobile-label">Procedure</span>
1615
- <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1616
- @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1617
- </div>
1618
- <div class="cell"><span class="mobile-label">Modifiers</span>
1619
- @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> }
1620
- <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)"/>
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
+ };
1993
+ }
1994
+ tone(section) {
1995
+ return mindBillBillTasksTone(section.tone);
1996
+ }
1997
+ pillBackground(index) {
1998
+ return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 20%,var(--s))`;
1999
+ }
2000
+ pillColor(index) {
2001
+ return `color-mix(in srgb,${mindBillBillTasksPillBase(index)} 62%,var(--t))`;
2002
+ }
2003
+ select(section, row, bucketIndex) {
2004
+ this.cellSelected.emit(mindBillBillTasksCell(section, row, bucketIndex, this.buckets));
2005
+ }
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>
1621
2021
  </div>
1622
- <div class="cell"><span class="mobile-label">Dx pointers</span>
1623
- <div class="dx">
1624
- @if (bill.diagnoses.length) {
1625
- @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1626
- <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>
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>
1627
2034
  }
1628
- } @else { <small>Add diagnoses above</small> }
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>
1629
2048
  </div>
1630
- </div>
1631
- <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1632
- <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1633
- <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1634
- </div> }
1635
- <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1636
- </fieldset>
1637
-
1638
- <fieldset><legend>Attachments</legend>
1639
- @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> }
1640
- <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>
1641
- </fieldset>
1642
-
1643
- @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1644
- <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>
1645
- @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> }
1646
- </form>
1647
- `, 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"] }] });
1648
2063
  }
1649
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillSubmissionComponent, decorators: [{
2064
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MindBillBillTasksDashboardComponent, decorators: [{
1650
2065
  type: Component,
1651
- args: [{ selector: "mindbill-bill-submission", standalone: true, imports: [CommonModule, FormsModule, MindBillComboBoxComponent, MindBillDateInputComponent], template: `
1652
- <form class="mbs" [ngStyle]="themeStyle" (submit)="submit($event)" novalidate>
1653
- <div class="intro"><div><h2>{{ heading }}</h2><p>{{ description }}</p></div><span><b>*</b> Required</span></div>
1654
-
1655
- <fieldset id="mbs-patient"><legend>Patient</legend><div class="grid">
1656
- <label [class.invalid]="bad('patient.firstName')">First name <b>*</b><input name="firstName" [(ngModel)]="bill.patient.firstName"></label>
1657
- <label [class.invalid]="bad('patient.lastName')">Last name <b>*</b><input name="lastName" [(ngModel)]="bill.patient.lastName"></label>
1658
- <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>
1659
- <label>Phone (optional)<input name="phone" type="tel" [(ngModel)]="bill.patient.phone"></label>
1660
- <label class="wide" [class.invalid]="bad('patient.address.line1')">Address <b>*</b><input name="address" [(ngModel)]="bill.patient.address.line1"></label>
1661
- <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>
1662
- <label [class.invalid]="bad('patient.address.city')">City <b>*</b><input name="city" [(ngModel)]="bill.patient.address.city"></label>
1663
- <label [class.invalid]="bad('patient.address.state')">State <b>*</b><input name="state" [(ngModel)]="bill.patient.address.state" maxlength="2"></label>
1664
- </div></fieldset>
1665
-
1666
- <fieldset id="mbs-claim"><legend>Injury &amp; claim</legend><div class="grid">
1667
- <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>
1668
- <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>
1669
- <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>
1670
- <label [class.invalid]="bad('claim.claimNumber')">Claim number <b>*</b><input name="claimNumber" [(ngModel)]="bill.claim.claimNumber"></label>
1671
- <label [class.invalid]="bad('claim.employer')">Employer <b>*</b><input name="employer" [(ngModel)]="bill.claim.employer"></label>
1672
- <div class="wide lookup" [class.invalid]="bad('claim.claimsAdministrator')"><label>Claims administrator <b>*</b></label>
1673
- <input name="payerQuery" [(ngModel)]="payerQuery" (focus)="searchPayers()" (input)="searchPayers()" placeholder="Search the MindBill payer directory…" autocomplete="off">
1674
- @if (payerBusy) { <small>Searching directory…</small> }
1675
- @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> }
1676
- @if (bill.claim.claimsAdministrator.id) { <small class="selected">✓ Routed to {{ bill.claim.claimsAdministrator.name }}</small> }
1677
- </div>
1678
- <label class="wide">Injury description (optional)<input name="description" [(ngModel)]="bill.claim.description"></label>
1679
- <div class="wide diagnosis" [class.invalid]="bad('diagnoses')"><label>Diagnosis codes (ICD-10) <b>*</b></label>
1680
- <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>
1681
- @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> }
1682
- <input name="diagnosisQuery" [(ngModel)]="diagnosisQuery" (focus)="searchDiagnoses(true)" (input)="searchDiagnoses(true)" placeholder="Search ICD-10 codes…" autocomplete="off">
1683
- @if (diagnosisBusy && !diagnosisResults.length) { <small>Searching ICD-10 directory…</small> }
1684
- @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> }
1685
- </div>
1686
- </div></fieldset>
1687
-
1688
- <fieldset id="mbs-provider"><legend>Providers &amp; place of service</legend><div class="grid">
1689
- <h3 class="wide">Billing provider</h3>
1690
- <label [class.invalid]="bad('billingProvider.name')">Billing provider name <b>*</b><input name="billingProvider" [(ngModel)]="bill.billingProvider.name"></label>
1691
- <label [class.invalid]="bad('billingProvider.taxId')">Tax ID <b>*</b><input name="taxId" [(ngModel)]="bill.billingProvider.taxId"></label>
1692
- <label [class.invalid]="bad('billingProvider.npi')">Billing NPI <b>*</b><input name="billingNpi" [(ngModel)]="bill.billingProvider.npi" inputmode="numeric" maxlength="10"></label>
1693
- <label [class.invalid]="bad('billingProvider.phone')">Billing phone <b>*</b><input name="billingPhone" type="tel" [(ngModel)]="bill.billingProvider.phone"></label>
1694
- <label class="wide" [class.invalid]="bad('billingProvider.address.line1')">Billing address <b>*</b><input name="billingAddress" [(ngModel)]="bill.billingProvider.address.line1"></label>
1695
- <label [class.invalid]="bad('billingProvider.address.postalCode')">Billing ZIP <b>*</b><input name="billingZip" [(ngModel)]="bill.billingProvider.address.postalCode" inputmode="numeric"></label>
1696
- <label [class.invalid]="bad('billingProvider.address.city')">Billing city <b>*</b><input name="billingCity" [(ngModel)]="bill.billingProvider.address.city"></label>
1697
- <label [class.invalid]="bad('billingProvider.address.state')">Billing state <b>*</b><input name="billingState" [(ngModel)]="bill.billingProvider.address.state" maxlength="2"></label>
1698
- <h3 class="wide">Rendering provider</h3>
1699
- <label [class.invalid]="bad('renderingProvider.name')">Rendering provider name <b>*</b><input name="renderingProvider" [(ngModel)]="bill.renderingProvider.name"></label>
1700
- <label [class.invalid]="bad('renderingProvider.npi')">Rendering provider NPI <b>*</b><input name="renderingNpi" [(ngModel)]="bill.renderingProvider.npi" inputmode="numeric" maxlength="10"></label>
1701
- <div class="wide" [class.invalid]="bad('renderingProvider.taxonomy')"><label>Rendering taxonomy <b>*</b></label>
1702
- <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)"/>
1703
- <small>Search by specialty name or 10-character taxonomy code.</small>
1704
- </div>
1705
- <h3 class="wide">Place of service</h3>
1706
- <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>
1707
- <label class="wide" [class.invalid]="bad('serviceLocation.address.line1')">Service address <b>*</b><input name="serviceAddress" [(ngModel)]="bill.serviceLocation.address.line1"></label>
1708
- <label [class.invalid]="bad('serviceLocation.address.postalCode')">Service ZIP <b>*</b><input name="serviceZip" [(ngModel)]="bill.serviceLocation.address.postalCode" inputmode="numeric"></label>
1709
- <label [class.invalid]="bad('serviceLocation.address.city')">Service city <b>*</b><input name="serviceCity" [(ngModel)]="bill.serviceLocation.address.city"></label>
1710
- <label [class.invalid]="bad('serviceLocation.address.state')">Service state <b>*</b><input name="serviceState" [(ngModel)]="bill.serviceLocation.address.state" maxlength="2"></label>
1711
- </div></fieldset>
1712
-
1713
- <fieldset id="mbs-lines" [class.invalid-set]="bad('serviceLines')"><legend>Service lines</legend>
1714
- <div class="line-head"><span>Procedure code *</span><span>Modifiers</span><span>Dx</span><span>Units *</span><span>Allowed</span><span></span></div>
1715
- @for (line of bill.serviceLines; track $index; let index = $index) { <div class="service-line">
1716
- <div class="cell"><span class="mobile-label">Procedure</span>
1717
- <mindbill-combo-box [ariaLabel]="'Procedure code ' + (index + 1)" [value]="line.code" placeholder="Search or enter code…" [options]="procedureComboOptions" [createOption]="customProcedureOption" (selected)="selectProcedure(index, $event)"/>
1718
- @if (line.code) { <small>{{ procedureDescription(line.code) || 'Custom CPT, HCPCS, or medical-legal code' }}</small> }
1719
- </div>
1720
- <div class="cell"><span class="mobile-label">Modifiers</span>
1721
- @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> }
1722
- <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>
1723
2080
  </div>
1724
- <div class="cell"><span class="mobile-label">Dx pointers</span>
1725
- <div class="dx">
1726
- @if (bill.diagnoses.length) {
1727
- @for (code of bill.diagnoses; track code; let dxIndex = $index) {
1728
- <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>
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>
1729
2093
  }
1730
- } @else { <small>Add diagnoses above</small> }
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>
1731
2107
  </div>
1732
- </div>
1733
- <label><span class="mobile-label">Units</span><input [name]="'units'+index" type="number" min="1" [(ngModel)]="line.units"></label>
1734
- <strong>{{ allowed(line) == null ? '—' : (allowed(line) | currency) }}</strong>
1735
- <button class="remove" type="button" (click)="removeLine(index)" aria-label="Remove service line">×</button>
1736
- </div> }
1737
- <div class="total"><span>Total</span><strong>{{ totalAllowed | currency }}</strong></div>
1738
- </fieldset>
1739
-
1740
- <fieldset><legend>Attachments</legend>
1741
- @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> }
1742
- <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>
1743
- </fieldset>
1744
-
1745
- @if (errorMessage) { <div class="error" role="alert">{{ errorMessage }}</div> }
1746
- <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>
1747
- @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> }
1748
- </form>
1749
- `, 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"] }]
1750
- }], propDecorators: { initialBill: [{
1751
- type: Input
1752
- }], attachments: [{
1753
- type: Input
1754
- }], appearance: [{
1755
- type: Input
1756
- }], sessionEndpoint: [{
1757
- type: Input
1758
- }], 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: [{
1759
2123
  type: Input
1760
- }], apiBaseUrl: [{
2124
+ }], buckets: [{
1761
2125
  type: Input
1762
2126
  }], heading: [{
1763
2127
  type: Input
1764
- }], description: [{
1765
- type: Input
1766
- }], submitLabel: [{
1767
- type: Input
1768
- }], procedureOptions: [{
1769
- type: Input
1770
- }], modifierOptions: [{
2128
+ }], grandTotalLabel: [{
1771
2129
  type: Input
1772
- }], taxonomyOptions: [{
2130
+ }], emptyLabel: [{
1773
2131
  type: Input
1774
- }], submitter: [{
2132
+ }], appearance: [{
1775
2133
  type: Input
1776
- }], submitted: [{
1777
- type: Output
1778
- }], submissionError: [{
1779
- type: Output
1780
- }], billChange: [{
2134
+ }], cellSelected: [{
1781
2135
  type: Output
1782
- }], onWindowDragOver: [{
1783
- type: HostListener,
1784
- args: ["window:dragover", ["$event"]]
1785
- }], onWindowDragLeave: [{
1786
- type: HostListener,
1787
- args: ["window:dragleave", ["$event"]]
1788
- }], onWindowDrop: [{
1789
- type: HostListener,
1790
- args: ["window:drop", ["$event"]]
1791
2136
  }] } });
1792
2137
 
1793
2138
  const THEME$1 = {
@@ -2234,5 +2579,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2234
2579
  * Generated bundle index. Do not edit.
2235
2580
  */
2236
2581
 
2237
- export { MINDBILL_STATUS_AGING_BUCKETS, MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillRejectionNoticeComponent, MindBillBillSubmissionComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillComboBoxComponent, MindBillDateInputComponent, MindBillLifecycleStore, MindBillOrganizationOnboardingComponent, MindBillStatusAgingMatrixComponent, billRejectionIssueSummary, billRejectionIssues, buildMindBillReportCsv, buildMindBillReportRows, buildMindBillStatusAgingCsv, buildMindBillStatusAgingMatrix, ensureTrailingProcedureLine, formatMindBillSubmissionDate, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, mindBillAngularAppearanceStyle, mindBillCustomProcedureOption, parseMindBillSubmissionDate, summarizeMindBillDashboard };
2582
+ export { MINDBILL_BILL_TASKS_PILL_BASES, MINDBILL_BILL_TASKS_TONES, MINDBILL_STATUS_AGING_BUCKETS, MindBillBillAgingSummaryComponent, MindBillBillLifecycleComponent, MindBillBillListComponent, MindBillBillRejectionNoticeComponent, MindBillBillSubmissionComponent, MindBillBillTasksDashboardComponent, MindBillBillingDashboardComponent, MindBillBillingManagementButtonComponent, MindBillBillingReportComponent, MindBillComboBoxComponent, MindBillDateInputComponent, MindBillLifecycleStore, MindBillOrganizationOnboardingComponent, MindBillStatusAgingMatrixComponent, billRejectionIssueSummary, billRejectionIssues, buildMindBillReportCsv, buildMindBillReportRows, buildMindBillStatusAgingCsv, buildMindBillStatusAgingMatrix, ensureTrailingProcedureLine, formatMindBillSubmissionDate, isEmptyProcedureLine, mindBillAgingBucket, mindBillAgingDays, mindBillAngularAppearanceStyle, mindBillBillTasksCell, mindBillBillTasksPillBase, mindBillBillTasksTone, mindBillCustomProcedureOption, parseMindBillSubmissionDate, summarizeMindBillDashboard };
2238
2583
  //# sourceMappingURL=mindbill-angular.mjs.map